UNPKG

@bernierllc/content-autosave-manager

Version:

Automatic content saving with debouncing, retry logic, and conflict detection

352 lines (260 loc) 8.88 kB
# @bernierllc/content-autosave-manager Automatic content saving with debouncing, retry logic, and conflict detection for preventing data loss during editing sessions. ## Features - **Debounced Autosaving** - Save after inactivity period (configurable delay) - **Retry Logic** - Automatic retry on failures using exponential backoff from @bernierllc/backoff-retry - **Conflict Detection** - Detects version conflicts with configurable resolution strategies - **Version Tracking** - Maintains version numbers for draft content - **Status Management** - Real-time save status (idle, saving, saved, error) - **Event Hooks** - Listen to status changes and handle conflicts - **Multi-Content Support** - Manage autosave for multiple content items independently - **Framework Agnostic** - Works with any framework or save function ## Installation ```bash npm install @bernierllc/content-autosave-manager ``` ## Usage ### Basic Example ```typescript import { AutosaveManager } from '@bernierllc/content-autosave-manager'; // Create manager instance const manager = new AutosaveManager<string>({ debounceMs: 2000, // Wait 2 seconds of inactivity before saving maxRetries: 3, // Retry failed saves up to 3 times retryStrategy: 'exponential', enableVersioning: true, enableConflictDetection: true, }); // Define save function const saveFunction = async (contentId: string, content: string, version?: number) => { try { const response = await fetch(`/api/content/${contentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content, version }), }); if (!response.ok) { if (response.status === 409) { // Conflict detected const serverData = await response.json(); return { success: false, conflict: true, version: serverData.version, serverContent: serverData.content, }; } return { success: false, error: 'Save failed' }; } const data = await response.json(); return { success: true, version: data.version }; } catch (error) { return { success: false, error: error.message }; } }; // Enable autosave for content item manager.enable('blog-post-123', saveFunction); // Listen to status changes manager.onStatusChange((event) => { console.log(`Content ${event.contentId} status: ${event.status}`); if (event.status === 'saved') { console.log(`Saved at ${event.lastSaved}, version ${event.version}`); } if (event.status === 'error') { console.error(`Save error: ${event.error}`); } }); // Queue save (debounced) manager.queueSave('blog-post-123', 'Updated content...'); // Force immediate save (bypasses debouncing) const result = await manager.forceSave('blog-post-123', 'Final content'); if (result.success) { console.log('Content saved successfully'); } // Check current status const status = manager.getStatus('blog-post-123'); console.log(status); // { // contentId: 'blog-post-123', // status: 'saved', // version: 5, // lastSaved: Date, // error: undefined // } // Cleanup manager.disable('blog-post-123'); ``` ### Conflict Handling ```typescript import { AutosaveManager, ConflictEvent, ConflictResolution } from '@bernierllc/content-autosave-manager'; const manager = new AutosaveManager<string>(); // Register conflict handler manager.onConflict(async (event: ConflictEvent<string>) => { console.log('Conflict detected!'); console.log('Local version:', event.localVersion, event.localContent); console.log('Server version:', event.serverVersion, event.serverContent); // Show user a conflict resolution dialog const userChoice = await showConflictDialog(event); // Return resolution strategy if (userChoice === 'keep-local') { return 'local'; // Retry save with local content } else if (userChoice === 'use-server') { return 'server'; // Accept server version } else { return 'manual'; // User will resolve manually } }); manager.enable('document-1', saveFunction); ``` ### Multiple Content Items ```typescript const manager = new AutosaveManager<string>(); // Enable autosave for multiple content items manager.enable('blog-post-1', saveBlogPost); manager.enable('comment-1', saveComment); manager.enable('note-1', saveNote); // Queue saves independently manager.queueSave('blog-post-1', 'Blog content...'); manager.queueSave('comment-1', 'Comment text...'); manager.queueSave('note-1', 'Note content...'); // Each item autosaves independently with its own debounce timer ``` ### Per-Item Configuration ```typescript const manager = new AutosaveManager<string>({ debounceMs: 2000, // Default for all items maxRetries: 3, }); // Override config for specific items manager.enable('critical-doc', saveFunction, { debounceMs: 500, // Save more frequently for critical content maxRetries: 5, // More retries for important content }); manager.enable('draft-note', saveFunction, { debounceMs: 5000, // Save less frequently for drafts maxRetries: 1, // Fewer retries for non-critical content }); ``` ## API Reference ### AutosaveManager #### Constructor ```typescript new AutosaveManager<T>(config?: Partial<AutosaveConfig>) ``` Creates a new autosave manager instance. #### Methods ##### `enable(contentId, saveFunction, config?)` Enable autosave for a content item. ```typescript manager.enable( contentId: string, saveFunction: SaveFunction<T>, config?: Partial<AutosaveConfig> ): void ``` ##### `disable(contentId)` Disable autosave for a content item. ```typescript manager.disable(contentId: string): void ``` ##### `queueSave(contentId, content)` Queue content for autosave (triggers debounced save). ```typescript manager.queueSave(contentId: string, content: T): void ``` ##### `forceSave(contentId, content)` Force immediate save (bypasses debouncing). ```typescript manager.forceSave(contentId: string, content: T): Promise<SaveResult<T>> ``` ##### `getStatus(contentId)` Get current autosave status. ```typescript manager.getStatus(contentId: string): AutosaveStatusEvent | null ``` ##### `onConflict(handler)` Register conflict handler. ```typescript manager.onConflict(handler: ConflictHandler<T>): void ``` ##### `onStatusChange(listener)` Register status change listener. ```typescript manager.onStatusChange(listener: (event: AutosaveStatusEvent) => void): void ``` ##### `clear(contentId)` Clear all autosave state for content item. ```typescript manager.clear(contentId: string): void ``` ##### `destroy()` Cleanup all autosave state. ```typescript manager.destroy(): void ``` ## Configuration ### AutosaveConfig ```typescript interface AutosaveConfig { debounceMs?: number; // Default: 2000 maxRetries?: number; // Default: 3 retryStrategy?: 'exponential' | 'linear' | 'fibonacci'; // Default: 'exponential' enableVersioning?: boolean; // Default: true enableConflictDetection?: boolean; // Default: true } ``` ### Environment Variables ```bash AUTOSAVE_DEBOUNCE_MS=2000 AUTOSAVE_MAX_RETRIES=3 AUTOSAVE_RETRY_STRATEGY=exponential AUTOSAVE_ENABLE_VERSIONING=true AUTOSAVE_ENABLE_CONFLICTS=true ``` Configuration precedence (highest to lowest): 1. Per-item config (enable method) 2. Constructor options 3. Environment variables 4. Default values ## Types ### SaveFunction ```typescript type SaveFunction<T> = ( contentId: string, content: T, version?: number ) => Promise<SaveResult<T>>; ``` ### SaveResult ```typescript interface SaveResult<T> { success: boolean; version?: number; conflict?: boolean; serverContent?: T; error?: string; } ``` ### AutosaveStatus ```typescript type AutosaveStatus = 'idle' | 'saving' | 'saved' | 'error'; ``` ### ConflictResolution ```typescript type ConflictResolution = 'local' | 'server' | 'manual'; ``` ## Integration Status - **Logger**: planned - Autosave operations should be logged for debugging - **Docs-Suite**: ready - Complete API documentation with TypeDoc - **NeverHub**: optional - Can publish autosave events when available ## Dependencies - [@bernierllc/backoff-retry](../backoff-retry) - Exponential backoff retry logic ## Related Packages - [@bernierllc/content-editor-core](../content-editor-core) - Core editor functionality - [@bernierllc/blog-post-editor](../../service/blog-post-editor) - Blog post editing service - [@bernierllc/content-management-suite](../../suite/content-management-suite) - Complete CMS ## License Copyright (c) 2025 Bernier LLC. All rights reserved. This file is licensed to the client under a limited-use license. The client may use and modify this code *only within the scope of the project it was delivered for*. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.