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();
}