UNPKG

interact-fb

Version:

A comprehensive NPM package for interacting with Facebook Graph API across frontend frameworks (React, Vue, Angular, Svelte, Vanilla JS)

194 lines (163 loc) 5.93 kB
// src/graph.js import { getConfig } from './config.js'; import { createFacebookError, handleError, logError, FacebookTimeoutError } from './errors.js'; /** * Sleep utility for retry delays * @param {number} ms - Milliseconds to sleep */ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); /** * Enhanced Facebook Graph API client with retry logic and better error handling * @param {string} endpoint - API endpoint (without version prefix) * @param {string} accessToken - Facebook access token * @param {string} [method='GET'] - HTTP method * @param {object} [params={}] - Query parameters or request body * @param {object} [options={}] - Additional options * @param {string} [options.version] - API version override * @param {number} [options.timeout] - Request timeout override * @param {number} [options.retryAttempts] - Retry attempts override * @param {number} [options.retryDelay] - Retry delay override * @returns {Promise<object>} Facebook API response */ export async function graphAPI( endpoint, accessToken, method = 'GET', params = {}, options = {} ) { const config = getConfig(); const { version = config.version, timeout = config.timeout, retryAttempts = config.retryAttempts, retryDelay = config.retryDelay } = options; const url = new URL(`https://graph.facebook.com/${version}/${endpoint}`); const isGet = method.toUpperCase() === 'GET'; // For GET requests, add params to URL if (isGet && params && Object.keys(params).length > 0) { url.search = new URLSearchParams({ access_token: accessToken, ...params, }).toString(); } const requestOptions = { method, headers: { ...(isGet ? {} : { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, }), }, ...(isGet ? {} : { body: JSON.stringify(params) }), }; // Add timeout using AbortController const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); requestOptions.signal = controller.signal; let lastError; // Retry logic for (let attempt = 0; attempt <= retryAttempts; attempt++) { try { const response = await fetch(url.toString(), requestOptions); clearTimeout(timeoutId); const data = await response.json(); if (!response.ok) { const fbError = createFacebookError(data.error, response.status); // Don't retry on auth errors or client errors (4xx except rate limiting) if (response.status >= 400 && response.status < 500 && response.status !== 429) { throw fbError; } // Retry on server errors or rate limiting if (attempt < retryAttempts && (response.status >= 500 || response.status === 429)) { lastError = fbError; await sleep(retryDelay * Math.pow(2, attempt)); // Exponential backoff continue; } throw fbError; } return data; } catch (error) { clearTimeout(timeoutId); // Handle AbortError (timeout) if (error.name === 'AbortError') { const timeoutError = new FacebookTimeoutError( `Request timed out after ${timeout}ms`, 'REQUEST_TIMEOUT' ); if (attempt < retryAttempts) { lastError = timeoutError; await sleep(retryDelay * Math.pow(2, attempt)); continue; } throw timeoutError; } // Handle network errors if (error instanceof TypeError && error.message.includes('fetch')) { if (attempt < retryAttempts) { lastError = error; await sleep(retryDelay * Math.pow(2, attempt)); continue; } } // Don't retry on Facebook-specific errors that won't benefit from retry if (error.name === 'FacebookAuthError' || error.name === 'FacebookPermissionError') { throw error; } lastError = error; if (attempt < retryAttempts) { await sleep(retryDelay * Math.pow(2, attempt)); continue; } break; } } // If we get here, all retries failed const processedError = handleError( lastError, `graphAPI(${endpoint})`, { endpoint, method, attempt: retryAttempts + 1 } ); logError(processedError); throw processedError; } /** * Batch multiple Graph API requests * @param {Array} requests - Array of request objects * @param {string} accessToken - Facebook access token * @param {object} [options={}] - Additional options * @returns {Promise<Array>} Array of responses */ export async function batchGraphAPI(requests, accessToken, options = {}) { if (!Array.isArray(requests) || requests.length === 0) { throw new Error('Requests must be a non-empty array'); } // Facebook allows max 50 requests per batch const batchSize = 50; const results = []; for (let i = 0; i < requests.length; i += batchSize) { const batch = requests.slice(i, i + batchSize); const batchParam = batch.map((req, index) => ({ method: req.method || 'GET', relative_url: req.endpoint + (req.params ? `?${new URLSearchParams(req.params).toString()}` : ''), include_headers: false, name: req.name || `request_${i + index}` })); try { const response = await graphAPI( '', accessToken, 'POST', { batch: JSON.stringify(batchParam) }, options ); results.push(...response); } catch (error) { throw handleError(error, 'batchGraphAPI', { batchSize: batch.length }); } } return results; }