@oxyhq/services
Version:
Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀
281 lines (253 loc) • 7.92 kB
JavaScript
;
/**
* OxyServices Base Class
*
* Contains core infrastructure, HTTP client, request management, and error handling
*/
import { jwtDecode } from 'jwt-decode';
import { handleHttpError } from '../utils/errorUtils';
import { HttpService } from './HttpService';
import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
/**
* Base class for OxyServices with core infrastructure
*/
export class OxyServicesBase {
constructor(...args) {
const config = args[0];
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() {
HttpService.__resetTokensForTests();
}
/**
* Make a request with all performance optimizations
* This is the main method for all API calls - ensures authentication and performance features
*/
async makeRequest(method, url, data, options = {}) {
return this.httpService.request({
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
*/
getBaseURL() {
return this.httpService.getBaseURL();
}
/**
* Get the HTTP service instance
* Useful for advanced use cases where direct access to the HTTP service is needed
*/
getClient() {
return this.httpService;
}
/**
* Get performance metrics
*/
getMetrics() {
return this.httpService.getMetrics();
}
/**
* Clear request cache
*/
clearCache() {
this.httpService.clearCache();
}
/**
* Clear specific cache entry
*/
clearCacheEntry(key) {
this.httpService.clearCacheEntry(key);
}
/**
* Get cache statistics
*/
getCacheStats() {
return this.httpService.getCacheStats();
}
/**
* Get the configured Oxy Cloud (file storage/CDN) URL
*/
getCloudURL() {
return this.cloudURL;
}
/**
* Set authentication tokens
*/
setTokens(accessToken, refreshToken = '') {
this.httpService.setTokens(accessToken, refreshToken);
}
/**
* Clear stored authentication tokens
*/
clearTokens() {
this.httpService.clearTokens();
}
/**
* Get the current user ID from the access token
*/
getCurrentUserId() {
const accessToken = this.httpService.getAccessToken();
if (!accessToken) {
return null;
}
try {
const decoded = jwtDecode(accessToken);
return decoded.userId || decoded.id || null;
} catch (error) {
return null;
}
}
/**
* Check if the client has a valid access token (public method)
*/
hasValidToken() {
return this.httpService.hasAccessToken();
}
/**
* Get the raw access token (for constructing anchor URLs when needed)
*/
getAccessToken() {
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
* }
* ```
*/
async waitForAuth(timeoutMs = 5000) {
// 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
*/
async withAuthRetry(operation, operationName, options = {}) {
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) {
const isLastAttempt = attempt === maxRetries;
const errorObj = error && typeof error === 'object' ? error : 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() {
if (!this.hasValidToken()) {
return false;
}
try {
const res = await this.makeRequest('GET', '/api/auth/validate', undefined, {
cache: false,
retry: false
});
return res.valid === true;
} catch (error) {
return false;
}
}
/**
* Centralized error handling
*/
handleError(error) {
const api = handleHttpError(error);
const err = new Error(api.message);
err.code = api.code;
err.status = api.status;
err.details = api.details;
return err;
}
/**
* Health check endpoint
*/
async healthCheck() {
try {
return await this.makeRequest('GET', '/health', undefined, {
cache: false
});
} catch (error) {
throw this.handleError(error);
}
}
}
//# sourceMappingURL=OxyServices.base.js.map