@bernierllc/content-autosave-manager
Version:
Automatic content saving with debouncing, retry logic, and conflict detection
352 lines (260 loc) • 8.88 kB
Markdown
# @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.