@bernierllc/content-autosave-manager
v1.2.2
Published
Automatic content saving with debouncing, retry logic, and conflict detection
Readme
@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
npm install @bernierllc/content-autosave-managerUsage
Basic Example
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
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
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 timerPer-Item Configuration
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
new AutosaveManager<T>(config?: Partial<AutosaveConfig>)Creates a new autosave manager instance.
Methods
enable(contentId, saveFunction, config?)
Enable autosave for a content item.
manager.enable(
contentId: string,
saveFunction: SaveFunction<T>,
config?: Partial<AutosaveConfig>
): voiddisable(contentId)
Disable autosave for a content item.
manager.disable(contentId: string): voidqueueSave(contentId, content)
Queue content for autosave (triggers debounced save).
manager.queueSave(contentId: string, content: T): voidforceSave(contentId, content)
Force immediate save (bypasses debouncing).
manager.forceSave(contentId: string, content: T): Promise<SaveResult<T>>getStatus(contentId)
Get current autosave status.
manager.getStatus(contentId: string): AutosaveStatusEvent | nullonConflict(handler)
Register conflict handler.
manager.onConflict(handler: ConflictHandler<T>): voidonStatusChange(listener)
Register status change listener.
manager.onStatusChange(listener: (event: AutosaveStatusEvent) => void): voidclear(contentId)
Clear all autosave state for content item.
manager.clear(contentId: string): voiddestroy()
Cleanup all autosave state.
manager.destroy(): voidConfiguration
AutosaveConfig
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
AUTOSAVE_DEBOUNCE_MS=2000
AUTOSAVE_MAX_RETRIES=3
AUTOSAVE_RETRY_STRATEGY=exponential
AUTOSAVE_ENABLE_VERSIONING=true
AUTOSAVE_ENABLE_CONFLICTS=trueConfiguration precedence (highest to lowest):
- Per-item config (enable method)
- Constructor options
- Environment variables
- Default values
Types
SaveFunction
type SaveFunction<T> = (
contentId: string,
content: T,
version?: number
) => Promise<SaveResult<T>>;SaveResult
interface SaveResult<T> {
success: boolean;
version?: number;
conflict?: boolean;
serverContent?: T;
error?: string;
}AutosaveStatus
type AutosaveStatus = 'idle' | 'saving' | 'saved' | 'error';ConflictResolution
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 - Exponential backoff retry logic
Related Packages
- @bernierllc/content-editor-core - Core editor functionality
- @bernierllc/blog-post-editor - Blog post editing service
- @bernierllc/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.
