Skip to main content
Glama
payload-optimizer.js•12.1 kB
#!/usr/bin/env node /** * Payload Optimizer for API Save * Optimizes composition JSON to prevent 500 errors and content truncation */ export class PayloadOptimizer { constructor() { this.MAX_PAYLOAD_SIZE = 25000; // bytes - conservative limit this.MAX_TEXT_LENGTH = 2000; // characters per text widget this.MAX_WIDGETS = 12; // maximum widgets per composition } /** * Main optimization entry point */ optimizeComposition(composerJSON) { console.error('[PAYLOAD_OPTIMIZER] Starting composition optimization'); const originalSize = this.calculatePayloadSize(composerJSON); console.error(`[PAYLOAD_OPTIMIZER] Original size: ${originalSize} bytes`); if (originalSize <= this.MAX_PAYLOAD_SIZE) { console.error('[PAYLOAD_OPTIMIZER] Size within limits, no optimization needed'); return { optimized: false, originalSize: originalSize, finalSize: originalSize, composition: composerJSON, optimizations: [] }; } let optimizedComposition = JSON.parse(JSON.stringify(composerJSON)); const optimizations = []; // Step 1: Optimize text content const textOptimization = this.optimizeTextContent(optimizedComposition); if (textOptimization.changed) { optimizations.push('text_content_optimization'); optimizedComposition = textOptimization.composition; } // Step 2: Remove unnecessary formatting const formatOptimization = this.removeUnnecessaryFormatting(optimizedComposition); if (formatOptimization.changed) { optimizations.push('formatting_cleanup'); optimizedComposition = formatOptimization.composition; } // Step 3: Optimize metadata and assets const metadataOptimization = this.optimizeMetadata(optimizedComposition); if (metadataOptimization.changed) { optimizations.push('metadata_optimization'); optimizedComposition = metadataOptimization.composition; } // Step 4: Widget prioritization if still too large const finalSize = this.calculatePayloadSize(optimizedComposition); if (finalSize > this.MAX_PAYLOAD_SIZE) { const widgetOptimization = this.prioritizeWidgets(optimizedComposition); optimizations.push('widget_prioritization'); optimizedComposition = widgetOptimization.composition; } const finalOptimizedSize = this.calculatePayloadSize(optimizedComposition); console.error(`[PAYLOAD_OPTIMIZER] Optimization complete:`); console.error(` Original: ${originalSize} bytes`); console.error(` Final: ${finalOptimizedSize} bytes`); console.error(` Reduction: ${((originalSize - finalOptimizedSize) / originalSize * 100).toFixed(1)}%`); console.error(` Optimizations: ${optimizations.join(', ')}`); return { optimized: true, originalSize: originalSize, finalSize: finalOptimizedSize, composition: optimizedComposition, optimizations: optimizations, reductionPercent: ((originalSize - finalOptimizedSize) / originalSize * 100).toFixed(1) }; } /** * Optimize text content in widgets */ optimizeTextContent(composition) { let changed = false; const optimizedStructure = composition.structure.map(widget => { if (widget.type === 'text-1' && widget.text) { const originalText = widget.text; const optimizedText = this.compressTextContent(originalText); if (optimizedText !== originalText) { changed = true; return { ...widget, text: optimizedText }; } } return widget; }); return { changed: changed, composition: { ...composition, structure: optimizedStructure } }; } /** * Compress text content while preserving readability */ compressTextContent(text) { if (!text || text.length <= this.MAX_TEXT_LENGTH) { return text; } // Remove excessive whitespace let compressed = text.replace(/\s+/g, ' ').trim(); // Remove redundant HTML attributes compressed = compressed.replace(/style="[^"]*"/g, ''); compressed = compressed.replace(/class="[^"]*"/g, ''); // If still too long, truncate intelligently if (compressed.length > this.MAX_TEXT_LENGTH) { compressed = this.intelligentTruncate(compressed, this.MAX_TEXT_LENGTH); } return compressed; } /** * Intelligently truncate text at sentence boundaries */ intelligentTruncate(text, maxLength) { if (text.length <= maxLength) return text; // Try to truncate at sentence boundary const truncated = text.substring(0, maxLength); const lastSentence = truncated.lastIndexOf('.</p>'); const lastParagraph = truncated.lastIndexOf('</p>'); if (lastSentence > maxLength * 0.8) { return text.substring(0, lastSentence + 5); // Include '</p>' } else if (lastParagraph > maxLength * 0.7) { return text.substring(0, lastParagraph + 4); // Include '</p>' } else { // Fallback: truncate at word boundary const lastSpace = truncated.lastIndexOf(' '); return text.substring(0, lastSpace) + '...</p>'; } } /** * Remove unnecessary formatting and whitespace */ removeUnnecessaryFormatting(composition) { let changed = false; // Remove excessive spacing in JSON const compactJSON = this.compactJSONStructure(composition); // Optimize widget structure const optimizedStructure = composition.structure.map(widget => { const optimized = { ...widget }; // Remove empty or default values if (optimized.content_title === null || optimized.content_title === '') { delete optimized.content_title; changed = true; } if (optimized.padding_top === 35 || optimized.padding_top === 25) { optimized.padding_top = 20; // Standardize padding changed = true; } if (optimized.padding_bottom === 35 || optimized.padding_bottom === 25) { optimized.padding_bottom = 20; // Standardize padding changed = true; } // Remove empty dam_assets arrays if (optimized.dam_assets && optimized.dam_assets.length === 0) { delete optimized.dam_assets; changed = true; } return optimized; }); return { changed: changed, composition: { ...compactJSON, structure: optimizedStructure } }; } /** * Optimize metadata and remove redundancy */ optimizeMetadata(composition) { let changed = false; const optimizedMetadata = { ...composition.metadata }; // Limit keywords length if (optimizedMetadata.keywords && optimizedMetadata.keywords.length > 8) { optimizedMetadata.keywords = optimizedMetadata.keywords.slice(0, 8); changed = true; } // Truncate description if too long if (optimizedMetadata.description && optimizedMetadata.description.length > 200) { optimizedMetadata.description = optimizedMetadata.description.substring(0, 197) + '...'; changed = true; } // Optimize assets array - remove duplicates let optimizedAssets = composition.assets || []; if (optimizedAssets.length > 0) { const uniqueAssets = [...new Set(optimizedAssets)]; if (uniqueAssets.length !== optimizedAssets.length) { optimizedAssets = uniqueAssets; changed = true; } } return { changed: changed, composition: { ...composition, metadata: optimizedMetadata, assets: optimizedAssets } }; } /** * Prioritize widgets and remove least important ones if necessary */ prioritizeWidgets(composition) { if (composition.structure.length <= this.MAX_WIDGETS) { return { composition: composition }; } console.error(`[PAYLOAD_OPTIMIZER] Too many widgets (${composition.structure.length}), prioritizing to ${this.MAX_WIDGETS}`); // Widget priority order (higher number = higher priority) const widgetPriority = { 'head-1': 10, // Always keep header 'text-1': 8, // Important content 'quiz-1': 9, // Assessment is important 'flashcards-1': 7, // Educational value 'image-1': 6, // Visual content 'list-1': 5, // Structured content 'hotspots-1': 4, // Interactive but complex 'gallery-1': 3, // Multiple images, larger 'video-1': 2 // Largest content }; // Sort widgets by priority const prioritizedWidgets = composition.structure .map((widget, index) => ({ widget: widget, priority: widgetPriority[widget.type] || 1, originalIndex: index })) .sort((a, b) => { // First by priority, then by original order if (b.priority !== a.priority) { return b.priority - a.priority; } return a.originalIndex - b.originalIndex; }) .slice(0, this.MAX_WIDGETS) .map(item => item.widget); const removedCount = composition.structure.length - prioritizedWidgets.length; console.error(`[PAYLOAD_OPTIMIZER] Removed ${removedCount} lower-priority widgets`); return { composition: { ...composition, structure: prioritizedWidgets } }; } /** * Compact JSON structure to remove unnecessary spacing */ compactJSONStructure(composition) { // This is primarily for internal processing // The main benefit comes from other optimizations return composition; } /** * Calculate approximate payload size in bytes */ calculatePayloadSize(composerJSON) { return JSON.stringify(composerJSON).length; } /** * Validate content integrity after optimization */ validateContentIntegrity(originalComposition, optimizedComposition) { const validation = { valid: true, issues: [], warnings: [] }; // Check essential widgets are preserved const originalTypes = originalComposition.structure.map(w => w.type); const optimizedTypes = optimizedComposition.structure.map(w => w.type); if (!optimizedTypes.includes('head-1')) { validation.issues.push('Header widget missing after optimization'); validation.valid = false; } if (originalTypes.includes('quiz-1') && !optimizedTypes.includes('quiz-1')) { validation.warnings.push('Assessment widget removed during optimization'); } // Check for content truncation issues optimizedComposition.structure.forEach((widget, index) => { if (widget.type === 'text-1' && widget.text) { if (widget.text.endsWith('...')) { validation.warnings.push(`Text widget ${index} was truncated`); } // Check for broken HTML const openTags = (widget.text.match(/</g) || []).length; const closeTags = (widget.text.match(/>/g) || []).length; if (openTags !== closeTags) { validation.issues.push(`Widget ${index} has malformed HTML after optimization`); validation.valid = false; } } }); return validation; } /** * Create optimization report */ createOptimizationReport(result) { const report = { success: result.optimized, originalSize: result.originalSize, finalSize: result.finalSize, sizeSavings: result.originalSize - result.finalSize, reductionPercent: result.reductionPercent, optimizations: result.optimizations, recommendations: [] }; if (result.finalSize > this.MAX_PAYLOAD_SIZE) { report.recommendations.push('Consider splitting lesson into multiple compositions'); report.recommendations.push('Reduce number of text widgets or content length'); } if (result.optimizations.includes('widget_prioritization')) { report.recommendations.push('Some widgets were removed - review lesson completeness'); } return report; } } export function createPayloadOptimizer() { return new PayloadOptimizer(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rkm097git/euconquisto-composer-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server