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
- Zero Content Loss: No content is ever lost during context transitions
- Reversible Operations: All changes can be undone or rolled back
- Incremental Preservation: Content is preserved at every significant change
- Context Awareness: Preservation strategies adapt to editing context
- 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.