From 13818d5844b0e24d5fdeeb45e919a6b5ab6827d3 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 2 Mar 2026 22:19:13 +0300 Subject: [PATCH] refactor(shared): update utilities, API layer, and types --- src/shared/api/api.test.ts | 282 ++++++++++++++++++ src/shared/api/api.ts | 83 ++++++ src/shared/api/queryClient.ts | 25 +- src/shared/lib/index.ts | 6 + .../ResponsiveProvider.svelte | 3 + src/shared/lib/storybook/MockIcon.svelte | 6 +- src/shared/lib/storybook/Providers.svelte | 11 +- .../buildQueryString/buildQueryString.ts | 50 ++-- .../lib/utils/clampNumber/clampNumber.ts | 21 +- src/shared/lib/utils/debounce/debounce.ts | 36 +-- .../getDecimalPlaces/getDecimalPlaces.ts | 21 +- src/shared/lib/utils/index.ts | 7 + .../roundToStepPrecision.ts | 22 +- .../lib/utils/smoothScroll/smoothScroll.ts | 17 +- src/shared/lib/utils/splitArray/splitArray.ts | 26 +- src/shared/lib/utils/throttle/throttle.ts | 24 +- src/shared/types/common.ts | 10 + 17 files changed, 554 insertions(+), 96 deletions(-) create mode 100644 src/shared/api/api.test.ts diff --git a/src/shared/api/api.test.ts b/src/shared/api/api.test.ts new file mode 100644 index 0000000..818c8ba --- /dev/null +++ b/src/shared/api/api.test.ts @@ -0,0 +1,282 @@ +/** + * Tests for API client + */ + +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from 'vitest'; +import { + ApiError, + api, +} from './api'; + +describe('api', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + describe('GET requests', () => { + test('should return data and status on successful request', async () => { + const mockData = { id: 1, name: 'Test' }; + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => mockData, + } as Response); + + const result = await api.get<{ id: number; name: string }>('/api/test'); + + expect(result.data).toEqual(mockData); + expect(result.status).toBe(200); + expect(fetch).toHaveBeenCalledWith( + '/api/test', + expect.objectContaining({ + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }), + ); + }); + }); + + describe('POST requests', () => { + test('should send JSON body and return response', async () => { + const requestBody = { name: 'Alice', email: 'alice@example.com' }; + const mockResponse = { id: 1, ...requestBody }; + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 201, + json: async () => mockResponse, + } as Response); + + const result = await api.post<{ id: number; name: string; email: string }>( + '/api/users', + requestBody, + ); + + expect(result.data).toEqual(mockResponse); + expect(result.status).toBe(201); + expect(fetch).toHaveBeenCalledWith( + '/api/users', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(requestBody), + headers: { 'Content-Type': 'application/json' }, + }), + ); + }); + + test('should handle POST with undefined body', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + } as Response); + + await api.post('/api/users', undefined); + + expect(fetch).toHaveBeenCalledWith( + '/api/users', + expect.objectContaining({ + method: 'POST', + body: undefined, + }), + ); + }); + }); + + describe('PUT requests', () => { + test('should send JSON body and return response', async () => { + const requestBody = { id: 1, name: 'Updated' }; + const mockResponse = { ...requestBody }; + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => mockResponse, + } as Response); + + const result = await api.put<{ id: number; name: string }>('/api/users/1', requestBody); + + expect(result.data).toEqual(mockResponse); + expect(result.status).toBe(200); + expect(fetch).toHaveBeenCalledWith( + '/api/users/1', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify(requestBody), + headers: { 'Content-Type': 'application/json' }, + }), + ); + }); + }); + + describe('DELETE requests', () => { + test('should return data and status on successful deletion', async () => { + const mockData = { message: 'Deleted successfully' }; + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => mockData, + } as Response); + + const result = await api.delete<{ message: string }>('/api/users/1'); + + expect(result.data).toEqual(mockData); + expect(result.status).toBe(200); + expect(fetch).toHaveBeenCalledWith( + '/api/users/1', + expect.objectContaining({ + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + }), + ); + }); + }); + + describe('error handling', () => { + test('should throw ApiError on non-OK response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + json: async () => ({ error: 'Resource not found' }), + } as Response); + + await expect(api.get('/api/not-found')).rejects.toThrow(ApiError); + }); + + test('should include status code in ApiError', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: async () => ({}), + } as Response); + + try { + await api.get('/api/error'); + expect.fail('Should have thrown ApiError'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + expect((error as ApiError).status).toBe(500); + } + }); + + test('should include message in ApiError', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 403, + statusText: 'Forbidden', + json: async () => ({}), + } as Response); + + try { + await api.get('/api/forbidden'); + expect.fail('Should have thrown ApiError'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + expect((error as ApiError).message).toBe('Request failed: Forbidden'); + } + }); + + test('should include response object in ApiError', async () => { + const mockResponse = { + ok: false, + status: 401, + statusText: 'Unauthorized', + json: async () => ({}), + } as Response; + vi.mocked(fetch).mockResolvedValueOnce(mockResponse); + + try { + await api.get('/api/unauthorized'); + expect.fail('Should have thrown ApiError'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + expect((error as ApiError).response).toBe(mockResponse); + } + }); + + test('should have correct error name', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: 'Bad Request', + json: async () => ({}), + } as Response); + + try { + await api.get('/api/bad-request'); + expect.fail('Should have thrown ApiError'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + expect((error as ApiError).name).toBe('ApiError'); + } + }); + }); + + describe('headers', () => { + test('should accept custom headers (replaces defaults)', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + } as Response); + + await api.get('/api/test', { + headers: { 'X-Custom-Header': 'custom-value' }, + }); + + expect(fetch).toHaveBeenCalledWith( + '/api/test', + expect.objectContaining({ + headers: { + 'X-Custom-Header': 'custom-value', + }, + }), + ); + }); + + test('should allow overriding default Content-Type', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({}), + } as Response); + + await api.get('/api/test', { + headers: { 'Content-Type': 'text/plain' }, + }); + + expect(fetch).toHaveBeenCalledWith( + '/api/test', + expect.objectContaining({ + headers: { 'Content-Type': 'text/plain' }, + }), + ); + }); + }); + + describe('empty response handling', () => { + test('should handle empty JSON response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 204, + json: async () => null, + } as Response); + + const result = await api.get('/api/empty'); + + expect(result.data).toBeNull(); + expect(result.status).toBe(204); + }); + }); +}); diff --git a/src/shared/api/api.ts b/src/shared/api/api.ts index a786e4c..40e12a8 100644 --- a/src/shared/api/api.ts +++ b/src/shared/api/api.ts @@ -1,9 +1,50 @@ +/** + * HTTP API client with error handling + * + * Provides a typed wrapper around fetch for JSON APIs. + * Automatically handles JSON serialization and error responses. + * + * @example + * ```ts + * import { api } from '$shared/api'; + * + * // GET request + * const users = await api.get('/api/users'); + * + * // POST request + * const newUser = await api.post('/api/users', { name: 'Alice' }); + * + * // Error handling + * try { + * const data = await api.get('/api/data'); + * } catch (error) { + * if (error instanceof ApiError) { + * console.error(error.status, error.message); + * } + * } + * ``` + */ + import type { ApiResponse } from '$shared/types/common'; +/** + * Custom error class for API failures + * + * Includes HTTP status code and the original Response object + * for debugging and error handling. + */ export class ApiError extends Error { + /** + * Creates a new API error + * @param status - HTTP status code + * @param message - Error message + * @param response - Original fetch Response object + */ constructor( + /** HTTP status code */ public status: number, message: string, + /** Original Response object for inspection */ public response?: Response, ) { super(message); @@ -11,6 +52,16 @@ export class ApiError extends Error { } } +/** + * Internal request handler + * + * Performs fetch with JSON headers and throws ApiError on failure. + * + * @param url - Request URL + * @param options - Fetch options (method, headers, body, etc.) + * @returns Response data and status code + * @throws ApiError when response is not OK + */ async function request( url: string, options?: RequestInit, @@ -39,9 +90,28 @@ async function request( }; } +/** + * API client methods + * + * Provides typed methods for common HTTP verbs. + * All methods return ApiResponse with data and status. + */ export const api = { + /** + * Performs a GET request + * @param url - Request URL + * @param options - Additional fetch options + * @returns Response data + */ get: (url: string, options?: RequestInit) => request(url, { ...options, method: 'GET' }), + /** + * Performs a POST request with JSON body + * @param url - Request URL + * @param body - Request body (will be JSON stringified) + * @param options - Additional fetch options + * @returns Response data + */ post: (url: string, body?: unknown, options?: RequestInit) => request(url, { ...options, @@ -49,6 +119,13 @@ export const api = { body: JSON.stringify(body), }), + /** + * Performs a PUT request with JSON body + * @param url - Request URL + * @param body - Request body (will be JSON stringified) + * @param options - Additional fetch options + * @returns Response data + */ put: (url: string, body?: unknown, options?: RequestInit) => request(url, { ...options, @@ -56,5 +133,11 @@ export const api = { body: JSON.stringify(body), }), + /** + * Performs a DELETE request + * @param url - Request URL + * @param options - Additional fetch options + * @returns Response data + */ delete: (url: string, options?: RequestInit) => request(url, { ...options, method: 'DELETE' }), }; diff --git a/src/shared/api/queryClient.ts b/src/shared/api/queryClient.ts index da8baf1..f6a25aa 100644 --- a/src/shared/api/queryClient.ts +++ b/src/shared/api/queryClient.ts @@ -1,24 +1,33 @@ import { QueryClient } from '@tanstack/query-core'; /** - * Query client instance + * TanStack Query client instance + * + * Configured for optimal caching and refetching behavior. + * Used by all font stores for data fetching and caching. + * + * Cache behavior: + * - Data stays fresh for 5 minutes (staleTime) + * - Unused data is garbage collected after 10 minutes (gcTime) + * - No refetch on window focus (reduces unnecessary network requests) + * - 3 retries with exponential backoff on failure */ export const queryClient = new QueryClient({ defaultOptions: { queries: { - /** - * Default staleTime: 5 minutes - */ + /** Data remains fresh for 5 minutes after fetch */ staleTime: 5 * 60 * 1000, - /** - * Default gcTime: 10 minutes - */ + /** Unused cache entries are removed after 10 minutes */ gcTime: 10 * 60 * 1000, + /** Don't refetch when window regains focus */ refetchOnWindowFocus: false, + /** Refetch on mount if data is stale */ refetchOnMount: true, + /** Retry failed requests up to 3 times */ retry: 3, /** - * Exponential backoff + * Exponential backoff for retries + * 1s, 2s, 4s, 8s... capped at 30s */ retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), }, diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index dc62fee..5270695 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,3 +1,9 @@ +/** + * Shared library + * + * Reusable utilities, helpers, and providers for the application. + */ + export { type CharacterComparison, type ControlDataModel, diff --git a/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte b/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte index 6f205b1..e34ef35 100644 --- a/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte +++ b/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte @@ -11,6 +11,9 @@ import { setContext } from 'svelte'; import type { Snippet } from 'svelte'; interface Props { + /** + * Content snippet + */ children: Snippet; } diff --git a/src/shared/lib/storybook/MockIcon.svelte b/src/shared/lib/storybook/MockIcon.svelte index 82c91ee..d5b6fc4 100644 --- a/src/shared/lib/storybook/MockIcon.svelte +++ b/src/shared/lib/storybook/MockIcon.svelte @@ -15,15 +15,15 @@ import type { interface Props { /** - * The Lucide icon component + * Lucide icon component */ icon: Component; /** - * CSS classes to apply to the icon + * CSS classes */ class?: string; /** - * Additional icon-specific attributes + * Additional attributes */ attrs?: Record; } diff --git a/src/shared/lib/storybook/Providers.svelte b/src/shared/lib/storybook/Providers.svelte index 3f26917..2fe10e9 100644 --- a/src/shared/lib/storybook/Providers.svelte +++ b/src/shared/lib/storybook/Providers.svelte @@ -15,17 +15,22 @@ import { setContext } from 'svelte'; import type { Snippet } from 'svelte'; interface Props { + /** + * Content snippet + */ children: Snippet; /** - * Initial viewport width for the responsive context (default: 1280) + * Initial viewport width + * @default 1280 */ initialWidth?: number; /** - * Initial viewport height for the responsive context (default: 720) + * Initial viewport height + * @default 720 */ initialHeight?: number; /** - * Tooltip provider options + * Tooltip delay duration */ tooltipDelayDuration?: number; /** diff --git a/src/shared/lib/utils/buildQueryString/buildQueryString.ts b/src/shared/lib/utils/buildQueryString/buildQueryString.ts index fc09249..7c4817c 100644 --- a/src/shared/lib/utils/buildQueryString/buildQueryString.ts +++ b/src/shared/lib/utils/buildQueryString/buildQueryString.ts @@ -1,56 +1,46 @@ /** - * Build query string from URL parameters + * Builds URL query strings from parameter objects * - * Generic, type-safe function to build properly encoded query strings - * from URL parameters. Supports primitives, arrays, and optional values. - * - * @param params - Object containing query parameters - * @returns Encoded query string (empty string if no parameters) + * Creates properly encoded query strings from typed parameter objects. + * Handles primitives, arrays, and omits null/undefined values. * * @example * ```ts * buildQueryString({ category: 'serif', subsets: ['latin', 'latin-ext'] }) - * // Returns: "category=serif&subsets=latin&subsets=latin-ext" + * // Returns: "?category=serif&subsets=latin%2Clatin-ext" * * buildQueryString({ limit: 50, page: 1 }) - * // Returns: "limit=50&page=1" + * // Returns: "?limit=50&page=1" * * buildQueryString({}) * // Returns: "" * * buildQueryString({ search: 'hello world', active: true }) - * // Returns: "search=hello%20world&active=true" + * // Returns: "?search=hello%20world&active=true" * ``` */ /** - * Query parameter value type - * Supports primitives, arrays, and excludes null/undefined + * Supported query parameter value types */ export type QueryParamValue = string | number | boolean | string[] | number[]; /** - * Query parameters object + * Query parameters object with optional values */ export type QueryParams = Record; /** - * Build query string from URL parameters + * Builds a URL query string from a parameters object * * Handles: - * - Primitive values (string, number, boolean) - * - Arrays (multiple values with same key) - * - Optional values (excludes undefined/null) - * - Proper URL encoding - * - * Edge cases: - * - Empty object → empty string - * - No parameters → empty string - * - Nested objects → flattens to string representation - * - Special characters → proper encoding + * - Primitive values (string, number, boolean) - converted to strings + * - Arrays - comma-separated values + * - null/undefined - omitted from output + * - Special characters - URL encoded * * @param params - Object containing query parameters - * @returns Encoded query string (with "?" prefix if non-empty) + * @returns Encoded query string with "?" prefix, or empty string if no params */ export function buildQueryString(params: QueryParams): string { const searchParams = new URLSearchParams(); @@ -61,12 +51,14 @@ export function buildQueryString(params: QueryParams): string { continue; } - // Handle arrays (multiple values with same key) + // Handle arrays (comma-separated values) if (Array.isArray(value)) { - for (const item of value) { - if (item !== undefined && item !== null) { - searchParams.append(key, String(item)); - } + const joined = value + .filter(item => item !== undefined && item !== null) + .map(String) + .join(','); + if (joined) { + searchParams.append(key, joined); } } else { // Handle primitives diff --git a/src/shared/lib/utils/clampNumber/clampNumber.ts b/src/shared/lib/utils/clampNumber/clampNumber.ts index d3e28e4..8d13521 100644 --- a/src/shared/lib/utils/clampNumber/clampNumber.ts +++ b/src/shared/lib/utils/clampNumber/clampNumber.ts @@ -1,9 +1,20 @@ /** - * Clamp a number within a range. - * @param value The number to clamp. - * @param min minimum value - * @param max maximum value - * @returns The clamped number. + * Clamps a number within a specified range + * + * Ensures a value falls between minimum and maximum bounds. + * Values below min return min, values above max return max. + * + * @param value - The number to clamp + * @param min - Minimum allowed value (inclusive) + * @param max - Maximum allowed value (inclusive) + * @returns The clamped number + * + * @example + * ```ts + * clampNumber(5, 0, 10); // 5 + * clampNumber(-5, 0, 10); // 0 + * clampNumber(15, 0, 10); // 10 + * ``` */ export function clampNumber(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); diff --git a/src/shared/lib/utils/debounce/debounce.ts b/src/shared/lib/utils/debounce/debounce.ts index 58ed530..fc1f9f9 100644 --- a/src/shared/lib/utils/debounce/debounce.ts +++ b/src/shared/lib/utils/debounce/debounce.ts @@ -1,28 +1,24 @@ -/** - * ============================================================================ - * DEBOUNCE UTILITY - * ============================================================================ - * - * Creates a debounced function that delays execution until after wait milliseconds - * have elapsed since the last time it was invoked. - * - * @example - * ```typescript - * const debouncedSearch = debounce((query: string) => { - * console.log('Searching for:', query); - * }, 300); - * - * debouncedSearch('hello'); - * debouncedSearch('hello world'); // Only this will execute after 300ms - * ``` - */ - /** * Creates a debounced version of a function * + * Delays function execution until after `wait` milliseconds have elapsed + * since the last invocation. Useful for rate-limiting expensive operations + * like API calls or expensive DOM updates. + * + * @example + * ```ts + * const search = debounce((query: string) => { + * console.log('Searching for:', query); + * }, 300); + * + * search('a'); + * search('ab'); + * search('abc'); // Only this triggers the function after 300ms + * ``` + * * @param fn - The function to debounce * @param wait - The delay in milliseconds - * @returns A debounced function that will execute after the specified delay + * @returns A debounced function that executes after the delay */ export function debounce any>( fn: T, diff --git a/src/shared/lib/utils/getDecimalPlaces/getDecimalPlaces.ts b/src/shared/lib/utils/getDecimalPlaces/getDecimalPlaces.ts index 00451ac..367d861 100644 --- a/src/shared/lib/utils/getDecimalPlaces/getDecimalPlaces.ts +++ b/src/shared/lib/utils/getDecimalPlaces/getDecimalPlaces.ts @@ -1,14 +1,19 @@ /** - * Get the number of decimal places in a number + * Counts the number of decimal places in a number * - * For example: - * - 1 -> 0 - * - 0.1 -> 1 - * - 0.01 -> 2 - * - 0.05 -> 2 + * Returns the length of the decimal portion of a number. + * Used to determine precision for rounding operations. * - * @param step - The step number to analyze - * @returns The number of decimal places + * @param step - The number to analyze + * @returns The number of decimal places (0 for integers) + * + * @example + * ```ts + * getDecimalPlaces(1); // 0 + * getDecimalPlaces(0.1); // 1 + * getDecimalPlaces(0.01); // 2 + * getDecimalPlaces(0.005); // 3 + * ``` */ export function getDecimalPlaces(step: number): number { const str = step.toString(); diff --git a/src/shared/lib/utils/index.ts b/src/shared/lib/utils/index.ts index 904d58c..6d708ab 100644 --- a/src/shared/lib/utils/index.ts +++ b/src/shared/lib/utils/index.ts @@ -1,5 +1,12 @@ /** * Shared utility functions + * + * Provides common utilities for: + * - Number manipulation (clamping, precision, decimal places) + * - Function execution control (debounce, throttle) + * - Array operations (split by predicate) + * - URL handling (query string building) + * - DOM interactions (smooth scrolling) */ export { diff --git a/src/shared/lib/utils/roundToStepPrecision/roundToStepPrecision.ts b/src/shared/lib/utils/roundToStepPrecision/roundToStepPrecision.ts index 5ea6a24..f3eb854 100644 --- a/src/shared/lib/utils/roundToStepPrecision/roundToStepPrecision.ts +++ b/src/shared/lib/utils/roundToStepPrecision/roundToStepPrecision.ts @@ -1,19 +1,25 @@ import { getDecimalPlaces } from '$shared/lib/utils'; /** - * Round a value to the precision of the given step + * Rounds a value to match the precision of a given step * - * This fixes floating-point precision errors that occur with decimal steps. - * For example, with step=0.05, adding it repeatedly can produce values like - * 1.3499999999999999 instead of 1.35. + * Fixes floating-point precision errors that occur with decimal arithmetic. + * For example, repeatedly adding 0.05 can produce 1.3499999999999999 + * instead of 1.35 due to IEEE 754 floating-point representation. * - * We use toFixed() to round to the appropriate decimal places instead of - * Math.round(value / step) * step, which doesn't always work correctly - * due to floating-point arithmetic errors. + * Uses toFixed() instead of Math.round() for correct decimal rounding. * * @param value - The value to round - * @param step - The step to round to (defaults to 1) + * @param step - The step size to match precision of (default: 1) * @returns The rounded value + * + * @example + * ```ts + * roundToStepPrecision(1.3499999999999999, 0.05); // 1.35 + * roundToStepPrecision(1.2345, 0.01); // 1.23 + * roundToStepPrecision(1.2345, 0.1); // 1.2 + * roundToStepPrecision(1.5, 1); // 2 + * ``` */ export function roundToStepPrecision(value: number, step: number = 1): number { if (step <= 0) { diff --git a/src/shared/lib/utils/smoothScroll/smoothScroll.ts b/src/shared/lib/utils/smoothScroll/smoothScroll.ts index 9f3c616..00e90c8 100644 --- a/src/shared/lib/utils/smoothScroll/smoothScroll.ts +++ b/src/shared/lib/utils/smoothScroll/smoothScroll.ts @@ -1,6 +1,17 @@ /** - * Smoothly scrolls to the target element when an anchor element is clicked. - * @param node - The anchor element to listen for clicks on. + * Svelte action for smooth anchor scrolling + * + * Intercepts anchor link clicks to smoothly scroll to the target element + * instead of jumping instantly. Updates URL hash without causing scroll. + * + * @example + * ```svelte + * Go to Section + *
Section Content
+ * ``` + * + * @param node - The anchor element to attach to + * @returns Action object with destroy method */ export function smoothScroll(node: HTMLAnchorElement) { const handleClick = (event: MouseEvent) => { @@ -17,7 +28,7 @@ export function smoothScroll(node: HTMLAnchorElement) { block: 'start', }); - // Update URL hash without jumping + // Update URL hash without triggering scroll history.pushState(null, '', hash); } }; diff --git a/src/shared/lib/utils/splitArray/splitArray.ts b/src/shared/lib/utils/splitArray/splitArray.ts index 5dca87b..a3ea60c 100644 --- a/src/shared/lib/utils/splitArray/splitArray.ts +++ b/src/shared/lib/utils/splitArray/splitArray.ts @@ -1,8 +1,26 @@ /** - * Splits an array into two arrays based on a callback function. - * @param array The array to split. - * @param callback The callback function to determine which array to push each item to. - * @returns - An array containing two arrays, the first array contains items that passed the callback, the second array contains items that failed the callback. + * Splits an array into two groups based on a predicate + * + * Partitions an array into pass/fail groups using a callback function. + * More efficient than calling filter() twice. + * + * @param array - The array to split + * @param callback - Predicate function (true = first array, false = second) + * @returns Tuple of [passing items, failing items] + * + * @example + * ```ts + * const numbers = [1, 2, 3, 4, 5, 6]; + * const [even, odd] = splitArray(numbers, n => n % 2 === 0); + * // even: [2, 4, 6] + * // odd: [1, 3, 5] + * + * const users = [ + * { name: 'Alice', active: true }, + * { name: 'Bob', active: false } + * ]; + * const [active, inactive] = splitArray(users, u => u.active); + * ``` */ export function splitArray(array: T[], callback: (item: T) => boolean) { return array.reduce<[T[], T[]]>( diff --git a/src/shared/lib/utils/throttle/throttle.ts b/src/shared/lib/utils/throttle/throttle.ts index 4bb0c90..e48e0b3 100644 --- a/src/shared/lib/utils/throttle/throttle.ts +++ b/src/shared/lib/utils/throttle/throttle.ts @@ -1,9 +1,23 @@ /** - * Throttle function execution to a maximum frequency. + * Throttles a function to limit execution frequency * - * @param fn Function to throttle. - * @param wait Maximum time between function calls. - * @returns Throttled function. + * Ensures a function executes at most once per `wait` milliseconds. + * Unlike debounce, throttled functions execute on the leading edge + * and trailing edge if called repeatedly. + * + * @example + * ```ts + * const logScroll = throttle(() => { + * console.log('Scroll position:', window.scrollY); + * }, 100); + * + * window.addEventListener('scroll', logScroll); + * // Will log at most once every 100ms + * ``` + * + * @param fn - Function to throttle + * @param wait - Minimum time between executions in milliseconds + * @returns Throttled function */ export function throttle any>( fn: T, @@ -20,7 +34,7 @@ export function throttle any>( lastCall = now; fn(...args); } else { - // Schedule for end of wait period + // Schedule for end of wait period (trailing edge) if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { lastCall = Date.now(); diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts index 8bcc1b7..3dfe594 100644 --- a/src/shared/types/common.ts +++ b/src/shared/types/common.ts @@ -1,4 +1,14 @@ +/** + * Standard API response wrapper + * + * Encapsulates response data with HTTP status code + * for consistent error handling across API calls. + * + * @template T - Type of the response data + */ export interface ApiResponse { + /** Response payload data */ data: T; + /** HTTP status code */ status: number; }