magically-sdk
Version:
Official SDK for Magically - Build mobile apps with AI
198 lines (197 loc) • 8.02 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.APIClient = void 0;
const Logger_1 = require("./Logger");
class APIClient {
constructor(config, loggerPrefix = 'MagicallyAPI') {
this.apiKey = null;
// Infinite loop detection
this.callHistory = new Map();
this.LOOP_THRESHOLD = 10; // Max calls per method in time window
this.TIME_WINDOW = 5000; // 5 seconds
this.lastCleanup = 0; // Track last garbage collection
this.CLEANUP_INTERVAL = 30000; // Clean up every 30 seconds
this.config = config;
this.logger = new Logger_1.Logger(config.debug || false, loggerPrefix);
this.baseUrl = config.apiUrl || 'https://trymagically.com';
// Use API key from config if provided (for edge functions)
if (config.apiKey) {
this.apiKey = config.apiKey;
this.logger.info('API key provided in config - using API key authentication');
}
else if (typeof globalThis !== 'undefined' && 'MAGICALLY_API_KEY' in globalThis) {
// Fallback to global environment
this.apiKey = globalThis.MAGICALLY_API_KEY;
this.logger.info('API key detected in environment - using API key authentication');
}
}
/**
* Make an authenticated API request with automatic logging
*/
async request(endpoint, options, token) {
// Check for infinite loops before making request
this.checkForInfiniteLoop(options.operation || endpoint);
const startTime = Date.now();
let requestId = '';
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
// Use API key if available (edge environment), otherwise use provided token
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
this.logger.debug('Using API key authentication for request');
}
else if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const requestConfig = {
method: options.method,
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
};
// Log the request
requestId = this.logger.networkRequest(options.method, url, {
headers,
body: options.body,
operation: options.operation
});
try {
const response = await fetch(url, requestConfig);
const responseData = await response.json();
const duration = Date.now() - startTime;
if (!response.ok) {
// Log error response
this.logger.networkError(requestId, responseData, {
duration,
operation: options.operation
});
// Throw structured error
this.handleAPIError(responseData, `${options.operation || 'Request'} failed`);
}
// Log successful response
this.logger.networkResponse(requestId, {
status: response.status,
statusText: response.statusText,
duration,
data: this.sanitizeResponseData(responseData, options.operation),
operation: options.operation
});
return responseData;
}
catch (error) {
// Log network errors
if (requestId) {
this.logger.networkError(requestId, error, {
duration: Date.now() - startTime,
operation: options.operation
});
}
throw error;
}
}
/**
* Check if running in edge environment (has API key)
*/
isEdgeEnvironment() {
return this.apiKey !== null;
}
/**
* Sanitize response data for logging (avoid logging large arrays)
*/
sanitizeResponseData(data, operation) {
if (operation === 'query' && data.data && Array.isArray(data.data)) {
return {
...data,
dataCount: data.data.length,
data: '[Array of items]'
};
}
return data;
}
/**
* Check for infinite loops by tracking method call frequency
*/
checkForInfiniteLoop(methodName) {
const now = Date.now();
const cleanMethodName = methodName.replace(/[^a-zA-Z0-9:_-]/g, '_');
// Get or create call history for this method
if (!this.callHistory.has(cleanMethodName)) {
this.callHistory.set(cleanMethodName, []);
}
const calls = this.callHistory.get(cleanMethodName);
// Auto garbage collection - clean up stale method entries periodically
if (now - this.lastCleanup > this.CLEANUP_INTERVAL) {
this.garbageCollectCallHistory(now);
this.lastCleanup = now;
}
// Remove calls outside the time window
const cutoffTime = now - this.TIME_WINDOW;
const recentCalls = calls.filter(timestamp => timestamp > cutoffTime);
// Add current call
recentCalls.push(now);
// If no recent calls, remove the method entry entirely
if (recentCalls.length === 1) {
this.callHistory.set(cleanMethodName, recentCalls);
}
else {
this.callHistory.set(cleanMethodName, recentCalls);
}
// Check if we've exceeded the threshold
if (recentCalls.length > this.LOOP_THRESHOLD) {
// Use logger for structured warning
this.logger.error('🚨 INFINITE LOOP DETECTED', {
method: methodName,
callCount: recentCalls.length,
timeWindow: `${this.TIME_WINDOW / 1000}s`,
threshold: this.LOOP_THRESHOLD,
message: `Method '${methodName}' called ${recentCalls.length} times in ${this.TIME_WINDOW / 1000}s`
});
// Clear the history to prevent further spam
this.callHistory.set(cleanMethodName, []);
// Throw error to stop the loop
throw new Error(`Infinite loop detected in method: ${methodName}. Too many rapid calls (${recentCalls.length} in ${this.TIME_WINDOW / 1000}s)`);
}
}
/**
* Garbage collect stale call history entries to prevent memory leaks
*/
garbageCollectCallHistory(now) {
const cutoffTime = now - this.TIME_WINDOW;
const methodsToDelete = [];
// Check each method's call history
for (const [methodName, timestamps] of this.callHistory.entries()) {
// Filter out old calls
const recentCalls = timestamps.filter(timestamp => timestamp > cutoffTime);
if (recentCalls.length === 0) {
// No recent calls - mark for deletion
methodsToDelete.push(methodName);
}
else {
// Update with only recent calls
this.callHistory.set(methodName, recentCalls);
}
}
// Remove stale method entries
methodsToDelete.forEach(methodName => {
this.callHistory.delete(methodName);
});
if (methodsToDelete.length > 0) {
this.logger.debug('Garbage collected call history', {
removedMethods: methodsToDelete.length,
totalMethods: this.callHistory.size
});
}
}
/**
* Handle API errors with structured error information
*/
handleAPIError(errorData, fallbackMessage) {
// Create an error with the response data attached for the Logger
const error = new Error(errorData.error || errorData.message || fallbackMessage);
error.responseData = errorData; // Preserve the original error response for Logger
throw error;
}
}
exports.APIClient = APIClient;