UNPKG

novel-reader-sdk

Version:

SDK for Novel Reader API

295 lines (249 loc) 6.86 kB
/** * SDK Utility Functions * * Helper functions for URL building, query parameters, and response handling */ import type { DatabaseId, ApiError } from './types'; import { createErrorFromApiResponse, NetworkError, TimeoutError } from './errors'; // ===== URL Building Utilities ===== /** * Build URL with path parameters */ export function buildUrl(baseUrl: string, path: string, params?: Record<string, string>): string { let url = `${baseUrl.replace(/\/$/, '')}${path}`; if (params) { for (const [key, value] of Object.entries(params)) { url = url.replace(`:${key}`, encodeURIComponent(value)); } } return url; } /** * Build query string from parameters */ export function buildQueryString(params?: Record<string, unknown>): string { if (!params || Object.keys(params).length === 0) { return ''; } const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { searchParams.append(key, value as string); } } const queryString = searchParams.toString(); return queryString ? `?${queryString}` : ''; } /** * Combine URL with query parameters */ export function buildFullUrl( baseUrl: string, path: string, params?: Record<string, string>, query?: Record<string, unknown> | object ): string { const url = buildUrl(baseUrl, path, params); const queryString = buildQueryString(query as Record<string, unknown>); return `${url}${queryString}`; } // ===== HTTP Request Utilities ===== /** * Default fetch options for API requests */ export const defaultFetchOptions: RequestInit = { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, }; /** * Enhanced fetch with timeout and error handling */ export async function fetchWithTimeout( url: string, options: RequestInit & { timeout?: number } = {} ): Promise<Response> { const { timeout = 30000, ...fetchOptions } = options; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...defaultFetchOptions, ...fetchOptions, signal: controller.signal, }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new TimeoutError(`Request timeout after ${timeout}ms`); } throw new NetworkError('Network request failed', error); } } /** * Parse JSON response with error handling */ export async function parseJsonResponse<T>(response: Response): Promise<T> { const text = await response.text(); if (!text) { throw new NetworkError('Empty response body'); } try { const data = JSON.parse(text) as T | ApiError; // Check if response is an error if (!response.ok) { if (isApiError(data)) { throw createErrorFromApiResponse(data); } throw new NetworkError(`HTTP ${response.status}: ${response.statusText}`); } return data as T; } catch (error) { if (error instanceof Error && error.name === 'SyntaxError') { throw new NetworkError('Invalid JSON response', error); } throw error; } } /** * Type guard to check if response is an API error */ function isApiError(data: unknown): data is ApiError { if ( typeof data === 'object' && data !== null && Object.prototype.hasOwnProperty.call(data, 'error') ) { const error = (data as Record<string, unknown>).error; if ( typeof error === 'object' && error !== null && Object.prototype.hasOwnProperty.call(error, 'code') && Object.prototype.hasOwnProperty.call(error, 'message') ) { return true; } } return false; } // ===== Validation Utilities ===== /** * Validate database ID */ export function validateDatabaseId(id: string): asserts id is DatabaseId { if (!id || typeof id !== 'string') { throw new Error('Database ID must be a non-empty string'); } } /** * Validate URL format */ export function validateUrl(url: string): boolean { try { new URL(url); return true; } catch { return false; } } /** * Extract hostname from URL */ export function extractHostname(url: string): string { try { return new URL(url).hostname; } catch { throw new Error(`Invalid URL: ${url}`); } } // ===== Data Transformation Utilities ===== /** * Clean and normalize string data */ export function normalizeString(str: string | null | undefined): string { return (str ?? '').trim(); } /** * Convert array to comma-separated string */ export function arrayToString(arr: string[]): string { return arr.filter(Boolean).join(', '); } /** * Safe number parsing */ export function parseNumber(value: string | number | null | undefined): number | undefined { if (typeof value === 'number') { return value; } if (typeof value === 'string') { const parsed = parseFloat(value); return isNaN(parsed) ? undefined : parsed; } return undefined; } // ===== Retry Utilities ===== /** * Retry configuration */ export interface RetryConfig { maxAttempts: number; baseDelay: number; maxDelay: number; backoffFactor: number; } /** * Default retry configuration */ export const defaultRetryConfig: RetryConfig = { maxAttempts: 3, baseDelay: 1000, maxDelay: 10000, backoffFactor: 2, }; /** * Retry function with exponential backoff */ export async function withRetry<T>( fn: () => Promise<T>, config: Partial<RetryConfig> = {} ): Promise<T> { const { maxAttempts, baseDelay, maxDelay, backoffFactor } = { ...defaultRetryConfig, ...config, }; let lastError: Error = new Error('No attempts made'); for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt === maxAttempts) { break; } const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt - 1), maxDelay); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } // ===== Debug Utilities ===== /** * Log request details for debugging */ export function logRequest(method: string, url: string, data?: unknown): void { if (process.env.NODE_ENV === 'development') { console.log(`[ScrapingSDK] ${method} ${url}`, data ? { data } : ''); } } /** * Log response details for debugging */ export function logResponse(url: string, status: number, data?: unknown): void { if (process.env.NODE_ENV === 'development') { console.log(`[ScrapingSDK] Response ${status} from ${url}`, data ? { data } : ''); } }