UNPKG

evermark-sdk

Version:

Unified SDK for robust image handling and content management with storage orchestration

217 lines 8.18 kB
/** * Pure functions for resolving image sources from various input types */ /** * Default configuration for source resolution */ const DEFAULT_CONFIG = { maxSources: 5, defaultTimeout: 8000, includeIpfs: true, ipfsGateway: 'https://gateway.pinata.cloud/ipfs', mobileOptimization: false, priorityOverrides: {}, preferThumbnail: false, storageConfig: undefined, autoTransfer: false }; /** * Default timeouts by source type (in milliseconds) */ const DEFAULT_TIMEOUTS = { thumbnail: 3000, primary: 5000, fallback: 8000, placeholder: 1000 }; /** * Validates if a URL string is properly formatted */ export function isValidUrl(url) { if (!url || typeof url !== 'string') return false; try { const parsed = new URL(url); return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } catch { return false; } } /** * Validates if an IPFS hash is properly formatted */ export function isValidIpfsHash(hash) { if (!hash || typeof hash !== 'string') return false; // Check for various IPFS hash formats const ipfsHashRegex = /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|b[A-Za-z2-7]{58}|B[A-Z2-7]{58}|z[1-9A-HJ-NP-Za-km-z]{48}|F[0-9A-F]{50})$/; return ipfsHashRegex.test(hash); } /** * Creates an IPFS gateway URL from a hash */ export function createIpfsUrl(hash, gateway = DEFAULT_CONFIG.ipfsGateway) { if (!isValidIpfsHash(hash)) { throw new Error(`Invalid IPFS hash: ${hash}`); } const cleanGateway = gateway.replace(/\/$/, ''); return `${cleanGateway}/${hash}`; } /** * Determines storage provider from URL */ function getStorageProvider(url) { if (url.includes('supabase')) return 'supabase'; if (url.includes('ipfs') || url.includes('pinata')) return 'ipfs'; if (url.includes('cloudflare') || url.includes('cdn')) return 'cdn'; return 'external'; } /** * Extract image format from URL */ function extractImageFormat(url) { try { const pathname = new URL(url).pathname.toLowerCase(); const extension = pathname.split('.').pop(); if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension || '')) { return extension === 'jpeg' ? 'jpg' : extension; } } catch { // Invalid URL, ignore } return undefined; } /** * Resolves multiple image sources from input with intelligent prioritization */ export const resolveImageSources = (input, config = {}) => { const finalConfig = { ...DEFAULT_CONFIG, ...config }; const sources = []; // Helper to add a source with proper priority and metadata const addSource = (url, type, basePriority, size) => { if (!isValidUrl(url)) return; const customPriority = finalConfig.priorityOverrides[url]; const priority = customPriority ?? basePriority; const metadata = { storageProvider: getStorageProvider(url) }; if (size) { metadata.size = size; } const format = extractImageFormat(url); if (format) { metadata.format = format; } sources.push({ url, type, priority, timeout: DEFAULT_TIMEOUTS[type] || finalConfig.defaultTimeout, metadata }); }; // 1. Add thumbnail first if preferred or on mobile if ((input.preferThumbnail || finalConfig.preferThumbnail || finalConfig.mobileOptimization) && input.thumbnailUrl) { addSource(input.thumbnailUrl, 'thumbnail', 1, 'thumbnail'); } // 2. Add primary Supabase URL if (input.supabaseUrl) { const priority = (input.preferThumbnail || finalConfig.preferThumbnail) ? 2 : 1; addSource(input.supabaseUrl, 'primary', priority, 'large'); } // 3. Add thumbnail if not already added if (!(input.preferThumbnail || finalConfig.preferThumbnail) && !finalConfig.mobileOptimization && input.thumbnailUrl) { addSource(input.thumbnailUrl, 'thumbnail', 2, 'thumbnail'); } // 4. Add legacy processed URL if (input.processedUrl && input.processedUrl !== input.supabaseUrl) { addSource(input.processedUrl, 'fallback', 3, 'medium'); } // 5. Add external URLs if (input.externalUrls) { input.externalUrls.forEach((url, index) => { addSource(url, 'fallback', 4 + index, 'original'); }); } // 6. Add IPFS fallback if enabled if (finalConfig.includeIpfs && input.ipfsHash && isValidIpfsHash(input.ipfsHash)) { try { const ipfsUrl = createIpfsUrl(input.ipfsHash, finalConfig.ipfsGateway); addSource(ipfsUrl, 'fallback', 10, 'original'); // Always lowest priority } catch (error) { console.warn('Failed to create IPFS URL:', error); } } // Sort by priority and limit to maxSources const sortedSources = sources .sort((a, b) => a.priority - b.priority) .slice(0, finalConfig.maxSources); return sortedSources; }; /** * Creates a placeholder source for fallback display */ export function createPlaceholderSource(type, customUrl) { const placeholderUrls = { loading: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTgiIHN0cm9rZT0iIzM3NDE1MSIgc3Ryb2tlLXdpZHRoPSI0Ii8+CjxwYXRoIGQ9Ik0yMCAxMlYyMEw2IDI2IiBzdHJva2U9IiMzNzQxNTEiIHN0cm9rZS13aWR0aD0iNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo=', error: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTgiIHN0cm9rZT0iI0VGNDQ0NCIgc3Ryb2tlLXdpZHRoPSI0Ii8+CjxwYXRoIGQ9Ik0xNSAxNUwyNSAyNU0yNSAxNUwxNSAyNSIgc3Ryb2tlPSIjRUY0NDQ0IiBzdHJva2Utd2lkdGg9IjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K', empty: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iNCIgeT0iNCIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgc3Ryb2tlPSIjOUNBM0FGIiBzdHJva2Utd2lkdGg9IjIiLz4KPGNpcmNsZSBjeD0iMTQiIGN5PSIxNCIgcj0iMiIgZmlsbD0iIzlDQTNBRiIvPgo8cGF0aCBkPSJNMzIgMjhMMjYgMjJMMTAgMzIiIHN0cm9rZT0iIzlDQTNBRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+Cg==' }; return { url: customUrl || placeholderUrls[type], type: 'placeholder', priority: 999, timeout: 100, metadata: { storageProvider: 'external', size: 'thumbnail', format: 'svg' } }; } /** * Create default storage configuration */ export function createDefaultStorageConfig(supabaseUrl, supabaseKey, bucketName = 'evermark-images', existingClient) { if (!supabaseUrl || typeof supabaseUrl !== 'string') { throw new Error('Invalid supabaseUrl: must be a non-empty string'); } if (!supabaseKey || typeof supabaseKey !== 'string') { throw new Error('Invalid supabaseKey: must be a non-empty string'); } if (!bucketName || typeof bucketName !== 'string') { throw new Error('Invalid bucketName: must be a non-empty string'); } const config = { supabase: { url: supabaseUrl, anonKey: supabaseKey, bucketName, ...(existingClient && { client: existingClient }) }, ipfs: { gateway: 'https://gateway.pinata.cloud/ipfs', fallbackGateways: [ 'https://ipfs.io/ipfs', 'https://cloudflare-ipfs.com/ipfs', 'https://gateway.ipfs.io/ipfs' ], timeout: 10000 }, upload: { maxFileSize: 10 * 1024 * 1024, // 10MB allowedFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp'], generateThumbnails: true, thumbnailSize: { width: 400, height: 400 } } }; return config; } //# sourceMappingURL=url-resolver.js.map