Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Technical DocumentationContent Preservation System

Content Preservation System

Implementation Status: 🔮 Future Feature

This document describes a planned system that has not yet been implemented. The architecture and code examples below represent the target design for a future release. Current content saving uses standard Supabase database operations without the multi-layer preservation described here.

Overview

The MyStoryFlow Content Preservation System ensures zero data loss during transitions between editing contexts (story → chapter → book) while maintaining content integrity, version history, and seamless user experience.

Core Principles

  1. Zero Content Loss: No content is ever lost during context transitions
  2. Reversible Operations: All changes can be undone or rolled back
  3. Incremental Preservation: Content is preserved at every significant change
  4. Context Awareness: Preservation strategies adapt to editing context
  5. User Transparency: Users are informed of preservation actions

Content Preservation Architecture

1. Multi-Level Preservation Strategy

interface ContentPreservationLayer { level: 'browser' | 'client' | 'server' | 'backup' priority: number retention_period: string preservation_triggers: PreservationTrigger[] } const PRESERVATION_LAYERS: ContentPreservationLayer[] = [ { level: 'browser', priority: 1, retention_period: '24 hours', preservation_triggers: ['keystroke', 'focus_lost', 'context_switch'] }, { level: 'client', priority: 2, retention_period: '7 days', preservation_triggers: ['auto_save', 'manual_save', 'error_recovery'] }, { level: 'server', priority: 3, retention_period: '30 days', preservation_triggers: ['successful_save', 'version_checkpoint', 'context_transition'] }, { level: 'backup', priority: 4, retention_period: 'permanent', preservation_triggers: ['daily_backup', 'major_milestone', 'user_request'] } ]

2. Content Snapshot System

interface ContentSnapshot { id: string content_id: string content_type: 'story' | 'chapter' | 'book' snapshot_type: 'auto' | 'manual' | 'transition' | 'recovery' // Content data content_html: string content_blocks: ContentBlock[] metadata: ContentMetadata // Context information editor_context: EditorContext user_session: UserSession // Preservation metadata preservation_reason: string preservation_trigger: string can_restore: boolean // Timestamps created_at: string expires_at?: string // Relationships parent_snapshot_id?: string child_snapshot_ids: string[] // Verification content_hash: string integrity_verified: boolean }

Browser-Level Preservation (Real-time)

1. Keystroke-Level Recovery

class BrowserPreservation { private recoveryData: Map<string, RecoveryData> = new Map() private autoSaveTimer: NodeJS.Timeout | null = null // Save on every significant keystroke onContentChange(contentId: string, content: string, context: EditorContext) { const recoveryKey = `${context.mode}-${contentId}` this.recoveryData.set(recoveryKey, { content, context, timestamp: Date.now(), wordCount: this.calculateWordCount(content), changeCount: this.getChangeCount(recoveryKey) + 1 }) // Persist to localStorage localStorage.setItem(`mf-recovery-${recoveryKey}`, JSON.stringify({ content, context, timestamp: Date.now() })) // Schedule auto-save this.scheduleAutoSave(contentId, content, context) } // Restore content on editor initialization async restoreContent(contentId: string, context: EditorContext): Promise<string | null> { const recoveryKey = `${context.mode}-${contentId}` const stored = localStorage.getItem(`mf-recovery-${recoveryKey}`) if (!stored) return null try { const recoveryData = JSON.parse(stored) // Check if recovery data is recent (< 1 hour) if (Date.now() - recoveryData.timestamp < 3600000) { // Verify content hasn't been saved server-side since recovery const serverContent = await this.getServerContent(contentId, context) if (this.shouldOfferRecovery(recoveryData, serverContent)) { return recoveryData.content } } } catch (error) { console.warn('Failed to parse recovery data:', error) } return null } // Clear recovery data on successful save clearRecoveryData(contentId: string, context: EditorContext) { const recoveryKey = `${context.mode}-${contentId}` localStorage.removeItem(`mf-recovery-${recoveryKey}`) this.recoveryData.delete(recoveryKey) } }

2. Page Visibility Preservation

// Preserve content when user switches tabs or closes browser class PageVisibilityPreservation { constructor() { document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)) window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this)) } private handleVisibilityChange() { if (document.hidden) { // Save current content immediately this.emergencySave() } } private handleBeforeUnload(event: BeforeUnloadEvent) { const hasUnsavedChanges = this.checkUnsavedChanges() if (hasUnsavedChanges) { // Save to localStorage as fallback this.saveToLocalStorage() // Show browser confirmation event.preventDefault() event.returnValue = 'You have unsaved changes. Are you sure you want to leave?' return event.returnValue } } }

Client-Level Preservation (Auto-save)

1. Debounced Auto-Save with Context Awareness

class ClientPreservation { private debouncedSave: Map<string, DebouncedFunction> = new Map() private saveQueue: SaveOperation[] = [] private isOnline: boolean = navigator.onLine constructor() { window.addEventListener('online', () => this.handleOnline()) window.addEventListener('offline', () => this.handleOffline()) } // Context-aware auto-save setupAutoSave(contentId: string, context: EditorContext) { const saveKey = `${context.mode}-${contentId}` // Create debounced save function specific to this content const debouncedSave = debounce(async (content: string) => { if (!this.isOnline) { this.queueSaveOperation(contentId, content, context) return } try { await this.saveContent(contentId, content, context) this.notifySuccessfulSave(contentId, context) } catch (error) { this.handleSaveError(contentId, content, context, error) } }, 500) this.debouncedSave.set(saveKey, debouncedSave) } // Save content with context-specific logic private async saveContent(contentId: string, content: string, context: EditorContext) { const supabase = await createClient() // Create content snapshot before saving await this.createContentSnapshot(contentId, content, context, 'auto') switch (context.mode) { case 'story': return this.saveStoryContent(supabase, contentId, content, context) case 'chapter': return this.saveChapterContent(supabase, contentId, content, context) case 'book': return this.saveBookContent(supabase, contentId, content, context) } } // Queue operations for offline mode private queueSaveOperation(contentId: string, content: string, context: EditorContext) { this.saveQueue.push({ contentId, content, context, timestamp: Date.now(), retryCount: 0 }) // Save to IndexedDB for persistence this.saveToIndexedDB(contentId, content, context) } // Process queued operations when back online private async handleOnline() { this.isOnline = true while (this.saveQueue.length > 0) { const operation = this.saveQueue.shift()! try { await this.saveContent(operation.contentId, operation.content, operation.context) } catch (error) { operation.retryCount++ if (operation.retryCount < 3) { this.saveQueue.unshift(operation) // Retry } else { this.handleFailedOperation(operation, error) } } } } }

2. Conflict Resolution

interface ContentConflict { content_id: string local_content: string server_content: string local_timestamp: string server_timestamp: string conflict_type: 'timestamp' | 'content' | 'version' } class ConflictResolution { // Detect conflicts during save operations async detectConflict( contentId: string, localContent: string, localTimestamp: string ): Promise<ContentConflict | null> { const serverData = await this.getServerContent(contentId) if (!serverData) return null // Check timestamp conflict if (new Date(serverData.updated_at) > new Date(localTimestamp)) { return { content_id: contentId, local_content: localContent, server_content: serverData.content, local_timestamp: localTimestamp, server_timestamp: serverData.updated_at, conflict_type: 'timestamp' } } // Check content conflict if (serverData.content !== localContent) { return { content_id: contentId, local_content: localContent, server_content: serverData.content, local_timestamp: localTimestamp, server_timestamp: serverData.updated_at, conflict_type: 'content' } } return null } // Resolve conflicts with user input async resolveConflict( conflict: ContentConflict, resolution: 'use_local' | 'use_server' | 'merge' | 'create_version' ): Promise<string> { switch (resolution) { case 'use_local': return conflict.local_content case 'use_server': return conflict.server_content case 'merge': return this.mergeContent(conflict.local_content, conflict.server_content) case 'create_version': await this.createContentVersion(conflict) return conflict.local_content default: throw new Error(`Unknown resolution strategy: ${resolution}`) } } // Three-way merge for content private mergeContent(localContent: string, serverContent: string): string { // Use a diff algorithm to merge content intelligently const localBlocks = this.parseContentBlocks(localContent) const serverBlocks = this.parseContentBlocks(serverContent) // Merge blocks while preserving user intent const mergedBlocks = this.mergeContentBlocks(localBlocks, serverBlocks) return this.renderContentBlocks(mergedBlocks) } }

Server-Level Preservation (Versioning)

1. Content Version Management

-- Content versions table CREATE TABLE content_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_id UUID NOT NULL, content_type TEXT NOT NULL CHECK (content_type IN ('story', 'chapter', 'book')), version_number INTEGER NOT NULL, -- Content data content_html TEXT NOT NULL, content_blocks JSONB NOT NULL DEFAULT '[]', content_metadata JSONB NOT NULL DEFAULT '{}', -- Version metadata change_summary TEXT, change_type TEXT CHECK (change_type IN ('auto_save', 'manual_save', 'context_transition', 'template_change')), word_count INTEGER, character_count INTEGER, -- Context information editor_context JSONB NOT NULL, user_session_id TEXT, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_by UUID REFERENCES auth.users(id), -- Integrity content_hash TEXT NOT NULL, UNIQUE(content_id, content_type, version_number) ); -- Content snapshots for recovery CREATE TABLE content_snapshots ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_id UUID NOT NULL, content_type TEXT NOT NULL, snapshot_type TEXT NOT NULL CHECK (snapshot_type IN ('auto', 'manual', 'transition', 'recovery')), -- Snapshot data content_html TEXT NOT NULL, content_blocks JSONB NOT NULL DEFAULT '[]', metadata JSONB NOT NULL DEFAULT '{}', -- Preservation context editor_context JSONB NOT NULL, preservation_reason TEXT NOT NULL, preservation_trigger TEXT NOT NULL, -- Recovery information can_restore BOOLEAN DEFAULT true, parent_snapshot_id UUID REFERENCES content_snapshots(id), -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE, -- Integrity content_hash TEXT NOT NULL, integrity_verified BOOLEAN DEFAULT false );

2. Version Management Functions

class ServerPreservation { // Create new content version async createContentVersion( contentId: string, contentType: 'story' | 'chapter' | 'book', content: string, context: EditorContext, changeSummary?: string ): Promise<ContentVersion> { const supabase = await createClient() // Get current version number const { data: lastVersion } = await supabase .from('content_versions') .select('version_number') .eq('content_id', contentId) .eq('content_type', contentType) .order('version_number', { ascending: false }) .limit(1) .single() const versionNumber = (lastVersion?.version_number || 0) + 1 // Parse content blocks const contentBlocks = this.parseContentBlocks(content) const contentHash = this.calculateContentHash(content) // Create version record const { data: version, error } = await supabase .from('content_versions') .insert({ content_id: contentId, content_type: contentType, version_number: versionNumber, content_html: content, content_blocks: contentBlocks, content_metadata: this.extractMetadata(content), change_summary: changeSummary, change_type: this.determineChangeType(context), word_count: this.calculateWordCount(content), character_count: content.length, editor_context: context, content_hash: contentHash, created_by: context.userId }) .select() .single() if (error) throw new Error(`Failed to create version: ${error.message}`) // Clean up old versions (keep last 10) await this.cleanupOldVersions(contentId, contentType, 10) return version } // Create content snapshot for recovery async createContentSnapshot( contentId: string, content: string, context: EditorContext, snapshotType: 'auto' | 'manual' | 'transition' | 'recovery', preservationReason: string = 'Regular preservation' ): Promise<ContentSnapshot> { const supabase = await createClient() const contentHash = this.calculateContentHash(content) const contentBlocks = this.parseContentBlocks(content) // Calculate expiration (24 hours for auto, 7 days for manual) const expiresAt = new Date() expiresAt.setHours(expiresAt.getHours() + (snapshotType === 'auto' ? 24 : 168)) const { data: snapshot, error } = await supabase .from('content_snapshots') .insert({ content_id: contentId, content_type: context.mode, snapshot_type: snapshotType, content_html: content, content_blocks: contentBlocks, metadata: this.extractMetadata(content), editor_context: context, preservation_reason: preservationReason, preservation_trigger: this.getPresentationTrigger(context), expires_at: expiresAt.toISOString(), content_hash: contentHash }) .select() .single() if (error) throw new Error(`Failed to create snapshot: ${error.message}`) // Verify integrity await this.verifySnapshotIntegrity(snapshot.id) return snapshot } // Restore content from version or snapshot async restoreContent( contentId: string, restoreFrom: 'version' | 'snapshot', restoreId: string, context: EditorContext ): Promise<string> { const supabase = await createClient() let contentData if (restoreFrom === 'version') { const { data, error } = await supabase .from('content_versions') .select('content_html, content_hash') .eq('id', restoreId) .single() if (error) throw new Error(`Failed to load version: ${error.message}`) contentData = data } else { const { data, error } = await supabase .from('content_snapshots') .select('content_html, content_hash, can_restore') .eq('id', restoreId) .single() if (error) throw new Error(`Failed to load snapshot: ${error.message}`) if (!data.can_restore) throw new Error('Snapshot cannot be restored') contentData = data } // Verify content integrity const calculatedHash = this.calculateContentHash(contentData.content_html) if (calculatedHash !== contentData.content_hash) { throw new Error('Content integrity check failed') } // Create backup of current content before restore await this.createContentSnapshot( contentId, await this.getCurrentContent(contentId, context), context, 'manual', `Backup before restore from ${restoreFrom} ${restoreId}` ) return contentData.content_html } }

Context Transition Preservation

1. Seamless Context Switching

class ContextTransitionPreservation { // Preserve content during context transitions async preserveForTransition( fromContext: EditorContext, toContext: EditorContext, content: string ): Promise<TransitionResult> { // Create transition snapshot const transitionSnapshot = await this.createTransitionSnapshot( fromContext, toContext, content ) // Validate content for target context const validationResult = await this.validateContentForContext(content, toContext) // Transform content if needed const transformedContent = await this.transformContentForContext( content, fromContext, toContext ) // Store transition record const transitionRecord = await this.createTransitionRecord({ from_context: fromContext, to_context: toContext, original_content: content, transformed_content: transformedContent, snapshot_id: transitionSnapshot.id, validation_result: validationResult }) return { success: true, transformedContent, transitionId: transitionRecord.id, preservationPoints: [transitionSnapshot.id], warnings: validationResult.warnings } } // Create specialized snapshot for transitions private async createTransitionSnapshot( fromContext: EditorContext, toContext: EditorContext, content: string ): Promise<ContentSnapshot> { return this.createContentSnapshot( fromContext.contentId, content, fromContext, 'transition', `Transition from ${fromContext.mode} to ${toContext.mode}` ) } // Transform content for different contexts private async transformContentForContext( content: string, fromContext: EditorContext, toContext: EditorContext ): Promise<string> { // Parse content blocks const blocks = this.parseContentBlocks(content) // Apply context-specific transformations const transformedBlocks = blocks.map(block => { return this.transformBlock(block, fromContext, toContext) }) // Re-render content return this.renderContentBlocks(transformedBlocks) } // Validate content compatibility with target context private async validateContentForContext( content: string, context: EditorContext ): Promise<ValidationResult> { const blocks = this.parseContentBlocks(content) const warnings: string[] = [] const errors: string[] = [] for (const block of blocks) { const blockDefinition = this.getBlockDefinition(block.type) if (!blockDefinition.availableInModes.includes(context.mode)) { warnings.push(`Block type "${block.type}" may not render correctly in ${context.mode} mode`) } // Validate block content for target context const blockValidation = this.validateBlockForContext(block, context) warnings.push(...blockValidation.warnings) errors.push(...blockValidation.errors) } return { isValid: errors.length === 0, warnings, errors } } }

2. Rollback Mechanisms

class RollbackSystem { // Rollback to previous state async rollback( contentId: string, context: EditorContext, rollbackTo: 'last_save' | 'last_version' | 'specific_snapshot', targetId?: string ): Promise<RollbackResult> { try { // Create safety snapshot before rollback const currentContent = await this.getCurrentContent(contentId, context) const safetySnapshot = await this.createContentSnapshot( contentId, currentContent, context, 'manual', 'Safety snapshot before rollback' ) let restoredContent: string switch (rollbackTo) { case 'last_save': restoredContent = await this.getLastSavedContent(contentId, context) break case 'last_version': restoredContent = await this.getLastVersionContent(contentId, context) break case 'specific_snapshot': if (!targetId) throw new Error('Target snapshot ID required') restoredContent = await this.restoreFromSnapshot(targetId) break default: throw new Error(`Unknown rollback target: ${rollbackTo}`) } // Apply rollback await this.applyRollback(contentId, context, restoredContent) return { success: true, restoredContent, safetySnapshotId: safetySnapshot.id, rollbackTimestamp: new Date().toISOString() } } catch (error) { return { success: false, error: error.message, timestamp: new Date().toISOString() } } } // Undo last action async undoLastAction( contentId: string, context: EditorContext ): Promise<UndoResult> { const lastSnapshot = await this.getLastSnapshot(contentId, context.mode) if (!lastSnapshot) { return { success: false, error: 'No previous state available to undo to' } } return this.rollback(contentId, context, 'specific_snapshot', lastSnapshot.id) } }

User Interface Integration

1. Recovery Notifications

interface RecoveryNotification { type: 'auto_recovery' | 'conflict_detected' | 'rollback_available' content_id: string message: string actions: RecoveryAction[] priority: 'low' | 'medium' | 'high' auto_dismiss?: boolean } class RecoveryUI { // Show recovery options to user showRecoveryDialog(notification: RecoveryNotification): Promise<string> { return new Promise((resolve) => { const dialog = this.createRecoveryDialog(notification) dialog.onAction = (action: string) => { dialog.close() resolve(action) } dialog.show() }) } // Auto-recovery indicator showAutoRecoveryIndicator(recoveryData: RecoveryData) { const indicator = this.createIndicator({ type: 'success', message: 'Content recovered from previous session', duration: 5000, actions: [ { label: 'Dismiss', action: 'dismiss' }, { label: 'View Details', action: 'show_details' } ] }) indicator.show() } // Content preservation status showPreservationStatus(status: PreservationStatus) { const statusIndicator = this.createStatusIndicator({ type: status.type, message: status.message, lastSaved: status.lastSaved, wordCount: status.wordCount, autoSaveEnabled: status.autoSaveEnabled }) statusIndicator.update() } }

2. Version History Interface

class VersionHistoryUI { // Show version history panel showVersionHistory(contentId: string, contentType: string): void { const panel = this.createVersionPanel({ contentId, contentType, onRestore: this.handleVersionRestore.bind(this), onCompare: this.handleVersionCompare.bind(this), onDelete: this.handleVersionDelete.bind(this) }) panel.show() } // Compare versions showVersionComparison( version1: ContentVersion, version2: ContentVersion ): void { const comparison = this.createComparisonView({ leftVersion: version1, rightVersion: version2, showDiff: true, highlightChanges: true }) comparison.show() } // Timeline view of changes showChangeTimeline(contentId: string): void { const timeline = this.createTimelineView({ contentId, showVersions: true, showSnapshots: true, showTransitions: true, interactive: true }) timeline.show() } }

Performance Considerations

1. Intelligent Preservation Scheduling

class PreservationScheduler { private preservationQueue: PreservationTask[] = [] private isProcessing: boolean = false // Smart scheduling based on user activity schedulePreservation(task: PreservationTask) { // Prioritize based on task importance and user activity const priority = this.calculatePriority(task) task.priority = priority // Insert into queue maintaining priority order this.insertByPriority(task) // Process queue if not already processing if (!this.isProcessing) { this.processQueue() } } private calculatePriority(task: PreservationTask): number { let priority = 0 // User activity factor if (this.isUserActive()) priority += 10 // Content size factor priority += Math.min(task.contentSize / 1000, 20) // Time since last save factor const timeSinceLastSave = Date.now() - task.lastSaveTime priority += Math.min(timeSinceLastSave / 60000, 30) // Max 30 for 30+ minutes // Context transition factor if (task.type === 'context_transition') priority += 50 return priority } // Batch preservation operations private async processBatch(tasks: PreservationTask[]): Promise<void> { const batchOperations = tasks.map(task => this.createPreservationOperation(task)) try { await Promise.allSettled(batchOperations) } catch (error) { console.error('Batch preservation failed:', error) // Retry failed operations individually for (const task of tasks) { try { await this.createPreservationOperation(task) } catch (taskError) { this.handlePreservationError(task, taskError) } } } } }

2. Storage Optimization

class StorageOptimization { // Compress content for storage compressContent(content: string): string { // Use LZ-string or similar compression return LZString.compress(content) } // Decompress content for retrieval decompressContent(compressedContent: string): string { return LZString.decompress(compressedContent) } // Clean up expired snapshots async cleanupExpiredSnapshots(): Promise<void> { const supabase = await createClient() const { error } = await supabase .from('content_snapshots') .delete() .lt('expires_at', new Date().toISOString()) if (error) { console.error('Failed to cleanup expired snapshots:', error) } } // Optimize storage usage async optimizeStorage(userId: string): Promise<StorageStats> { const stats = await this.getStorageStats(userId) if (stats.usage > stats.quota * 0.8) { // Clean up old versions beyond retention policy await this.cleanupOldVersions(userId) // Compress large content blocks await this.compressLargeContent(userId) // Archive old snapshots await this.archiveOldSnapshots(userId) } return this.getStorageStats(userId) } }

The Content Preservation System ensures that no user content is ever lost while providing multiple layers of protection and recovery options. This creates a robust, trustworthy editing experience that users can depend on for their important stories and memories.