UNPKG

@oxyhq/services

Version:

Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀

329 lines (287 loc) • 9.28 kB
/** * OxyServices Base Class * * Contains core infrastructure, HTTP client, request management, and error handling */ import { jwtDecode } from 'jwt-decode'; import type { OxyConfig as OxyConfigBase, ApiError, User } from '../models/interfaces'; import { handleHttpError } from '../utils/errorUtils'; import { HttpService, type RequestOptions } from './HttpService'; import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors'; export interface OxyConfig extends OxyConfigBase { cloudURL?: string; } interface JwtPayload { exp?: number; userId?: string; id?: string; sessionId?: string; [key: string]: any; } /** * Base class for OxyServices with core infrastructure */ export class OxyServicesBase { public httpService: HttpService; public cloudURL: string; public config: OxyConfig; constructor(...args: any[]) { const config = args[0] as OxyConfig; if (!config || typeof config !== 'object') { throw new Error('OxyConfig is required'); } this.config = config; this.cloudURL = config.cloudURL || 'https://cloud.oxy.so'; // Initialize unified HTTP service (handles auth, caching, deduplication, queuing, retry) this.httpService = new HttpService(config); } // Test-only utility to reset global tokens between jest tests static __resetTokensForTests(): void { HttpService.__resetTokensForTests(); } /** * Make a request with all performance optimizations * This is the main method for all API calls - ensures authentication and performance features */ public async makeRequest<T>( method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', url: string, data?: any, options: RequestOptions = {} ): Promise<T> { return this.httpService.request<T>({ method, url, data: method !== 'GET' ? data : undefined, params: method === 'GET' ? data : undefined, ...options, }); } // ============================================================================ // CORE METHODS (HTTP Client, Token Management, Error Handling) // ============================================================================ /** * Get the configured Oxy API base URL */ public getBaseURL(): string { return this.httpService.getBaseURL(); } /** * Get the HTTP service instance * Useful for advanced use cases where direct access to the HTTP service is needed */ public getClient(): HttpService { return this.httpService; } /** * Get performance metrics */ public getMetrics() { return this.httpService.getMetrics(); } /** * Clear request cache */ public clearCache(): void { this.httpService.clearCache(); } /** * Clear specific cache entry */ public clearCacheEntry(key: string): void { this.httpService.clearCacheEntry(key); } /** * Get cache statistics */ public getCacheStats() { return this.httpService.getCacheStats(); } /** * Get the configured Oxy Cloud (file storage/CDN) URL */ public getCloudURL(): string { return this.cloudURL; } /** * Set authentication tokens */ public setTokens(accessToken: string, refreshToken = ''): void { this.httpService.setTokens(accessToken, refreshToken); } /** * Clear stored authentication tokens */ public clearTokens(): void { this.httpService.clearTokens(); } /** * Get the current user ID from the access token */ public getCurrentUserId(): string | null { const accessToken = this.httpService.getAccessToken(); if (!accessToken) { return null; } try { const decoded = jwtDecode<JwtPayload>(accessToken); return decoded.userId || decoded.id || null; } catch (error) { return null; } } /** * Check if the client has a valid access token (public method) */ public hasValidToken(): boolean { return this.httpService.hasAccessToken(); } /** * Get the raw access token (for constructing anchor URLs when needed) */ public getAccessToken(): string | null { return this.httpService.getAccessToken(); } /** * Wait for authentication to be ready * * Optimized for high-scale usage with immediate synchronous check and adaptive polling. * Returns immediately if token is already available (0ms delay), otherwise uses * adaptive polling that starts fast (50ms) and gradually increases to reduce CPU usage. * * @param timeoutMs Maximum time to wait in milliseconds (default: 5000ms) * @returns Promise that resolves to true if authentication is ready, false if timeout * * @example * ```typescript * const isReady = await oxyServices.waitForAuth(3000); * if (isReady) { * // Proceed with authenticated operations * } * ``` */ public async waitForAuth(timeoutMs = 5000): Promise<boolean> { // Immediate synchronous check - no delay if token is ready if (this.httpService.hasAccessToken()) { return true; } const startTime = performance.now(); const maxTime = startTime + timeoutMs; // Adaptive polling: start fast, then slow down to reduce CPU usage let pollInterval = 50; // Start with 50ms while (performance.now() < maxTime) { await new Promise(resolve => setTimeout(resolve, pollInterval)); if (this.httpService.hasAccessToken()) { return true; } // Increase interval after first few checks (adaptive polling) // This reduces CPU usage for long waits while maintaining responsiveness if (pollInterval < 200) { pollInterval = Math.min(pollInterval * 1.5, 200); } } return false; } /** * Execute a function with automatic authentication retry logic * This handles the common case where API calls are made before authentication completes */ public async withAuthRetry<T>( operation: () => Promise<T>, operationName: string, options: { maxRetries?: number; retryDelay?: number; authTimeoutMs?: number; } = {} ): Promise<T> { const { maxRetries = 2, retryDelay = 1000, authTimeoutMs = 5000 } = options; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { // First attempt: check if we have a token if (!this.httpService.hasAccessToken()) { if (attempt === 0) { // On first attempt, wait briefly for authentication to complete const authReady = await this.waitForAuth(authTimeoutMs); if (!authReady) { throw new OxyAuthenticationTimeoutError(operationName, authTimeoutMs); } } else { // On retry attempts, fail immediately if no token throw new OxyAuthenticationError( `Authentication required: ${operationName} requires a valid access token.`, 'AUTH_REQUIRED' ); } } // Execute the operation return await operation(); } catch (error: unknown) { const isLastAttempt = attempt === maxRetries; const errorObj = error && typeof error === 'object' ? error as { response?: { status?: number }; code?: string; message?: string } : null; const isAuthError = errorObj?.response?.status === 401 || errorObj?.code === 'MISSING_TOKEN' || errorObj?.message?.includes('Authentication') || error instanceof OxyAuthenticationError; if (isAuthError && !isLastAttempt && !(error instanceof OxyAuthenticationTimeoutError)) { await new Promise(resolve => setTimeout(resolve, retryDelay)); continue; } // If it's not an auth error, or it's the last attempt, throw the error if (error instanceof OxyAuthenticationError) { throw error; } throw this.handleError(error); } } // This should never be reached, but TypeScript requires it throw new OxyAuthenticationError(`${operationName} failed after ${maxRetries + 1} attempts`); } /** * Validate the current access token with the server */ async validate(): Promise<boolean> { if (!this.hasValidToken()) { return false; } try { const res = await this.makeRequest<{ valid: boolean }>('GET', '/api/auth/validate', undefined, { cache: false, retry: false, }); return res.valid === true; } catch (error) { return false; } } /** * Centralized error handling */ public handleError(error: unknown): Error { const api = handleHttpError(error); const err = new Error(api.message) as Error & { code?: string; status?: number; details?: Record<string, unknown> }; err.code = api.code; err.status = api.status; err.details = api.details; return err; } /** * Health check endpoint */ async healthCheck(): Promise<{ status: string; users?: number; timestamp?: string; [key: string]: any }> { try { return await this.makeRequest('GET', '/health', undefined, { cache: false }); } catch (error) { throw this.handleError(error); } } }