@classic-homes/offline-core
v0.1.3
Published
Framework-agnostic offline persistence and caching for Classic Theme
Readme
@classic-homes/offline-core
Framework-agnostic offline persistence and caching for Classic Theme applications. Provides file storage, form draft persistence, and data caching with automatic browser storage selection.
Features
- Three-tier storage fallback: OPFS → IndexedDB → Memory
- File storage: Store, retrieve, and manage file attachments
- Draft persistence: Save and restore form data with file references
- Data caching: TTL-based caching with stale-while-revalidate support
- Automatic cleanup: Expired entries are cleaned up automatically
- Type-safe: Full TypeScript support throughout
Installation
npm install @classic-homes/offline-coreQuick Start
import { initOfflineStorage, getOfflineStorage } from '@classic-homes/offline-core';
// Initialize at app startup
await initOfflineStorage();
// Get the storage instance
const storage = getOfflineStorage();
// Store a file
const file = new File(['content'], 'document.pdf');
const stored = await storage.files.storeFile(file, {
formKey: 'contact-form',
fieldName: 'attachments',
});
// Save a form draft
await storage.drafts.saveDraft('contact-form', {
name: 'John Doe',
email: '[email protected]',
});
// Cache data
await storage.cache.set('user:123', { name: 'John' }, { ttl: 60000 });API Reference
Initialization
initOfflineStorage(options?)
Initialize the offline storage system. Call once at application startup.
const storage = await initOfflineStorage({
preferredAdapters: ['opfs', 'indexeddb', 'memory'], // Fallback order
dbName: 'my-app-offline', // IndexedDB database name
autoCleanup: true, // Run cleanup on init
cleanupIntervalMs: 60000, // Cleanup interval (0 to disable)
});getOfflineStorage()
Get the initialized storage instance. Throws if not initialized.
isOfflineStorageInitialized()
Check if storage has been initialized.
File Storage
const files = storage.files;
// Validate files before storing
const errors = files.validateFiles(fileList, {
maxSizeBytes: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
acceptedTypes: ['image/*', 'application/pdf'],
});
// Store a file
const stored = await files.storeFile(file, {
formKey: 'form-id',
fieldName: 'attachment',
expirationMs: 7 * 24 * 60 * 60 * 1000, // 7 days
});
// Get a file with preview URL
const persisted = await files.getFile(stored.id, true);
// persisted.previewUrl for images
// Get files for a form
const formFiles = await files.getFiles({
formKey: 'form-id',
fieldName: 'attachment',
generatePreviews: true,
});
// Delete files
await files.deleteFile(fileId);
await files.deleteFormFiles('form-id');
// Convert to File objects for form submission
const fileMap = await files.getFilesAsFileObjects([id1, id2]);Draft Storage
const drafts = storage.drafts;
// Save a draft
await drafts.saveDraft(
'contact-form',
{ name: 'John', email: '[email protected]' },
{ attachments: ['file:1', 'file:2'] }, // File references
{ excludeFields: ['password'], expirationMs: 86400000 }
);
// Load a draft
const result = await drafts.loadDraft<FormData>('contact-form');
if (result.restored) {
const { data, fileRefs } = result.draft;
}
// Check if draft exists
const hasDraft = await drafts.hasDraft('contact-form');
// Clear a draft
await drafts.clearDraft('contact-form');
// Merge partial data
await drafts.mergeDraft('contact-form', { phone: '123-456-7890' });
// Update file references
await drafts.updateFileRefs('contact-form', 'photos', ['file:3']);
// List all drafts
const formKeys = await drafts.listDraftFormKeys();Cache
const cache = storage.cache;
// Set a value
await cache.set(
'key',
{ data: 'value' },
{
ttl: 60000, // 1 minute
tags: ['user', 'profile'],
}
);
// Get a value
const result = await cache.get<User>('key');
if (result.hit && !result.stale) {
console.log(result.value);
}
// Get or fetch
const user = await cache.getOrSet('user:123', () => fetchUser(123), {
ttl: 300000,
staleWhileRevalidate: true,
staleTime: 60000,
});
// Invalidate
await cache.invalidate('key');
await cache.invalidateByTag('user');
await cache.invalidateAll();
// Statistics
const stats = await cache.getStats();
// { hits, misses, staleHits, hitRate, entryCount }Maintenance
// Manual cleanup of expired entries
const { deletedCount, freedBytes } = await storage.cleanup();
// Get storage quota info
const { used, available } = await storage.getQuota();
// Dispose when done
await storage.dispose();Storage Adapters
The package automatically selects the best available storage:
| Adapter | Priority | Browser Support | Performance | | --------- | -------- | -------------------------------------- | -------------- | | OPFS | 1 | Chrome 86+, Firefox 111+, Safari 15.2+ | Excellent | | IndexedDB | 2 | All modern browsers | Good | | Memory | 3 | All environments | N/A (volatile) |
Safari OPFS
Safari requires OPFS operations to run in a Web Worker. The package handles this automatically with a built-in worker.
Browser Compatibility
| Feature | Chrome | Firefox | Safari | Edge | | --------- | ------ | ------- | ------ | ---- | | OPFS | 86+ | 111+ | 15.2+ | 86+ | | IndexedDB | Yes | Yes | Yes | Yes | | Memory | Yes | Yes | Yes | Yes |
Subpath Exports
// Main entry
import { initOfflineStorage } from '@classic-homes/offline-core';
// Storage adapters only
import { createMemoryAdapter } from '@classic-homes/offline-core/storage';
// File service only
import { createFileStorageService } from '@classic-homes/offline-core/files';
// Draft service only
import { createDraftStorageService } from '@classic-homes/offline-core/drafts';
// Cache service only
import { createCacheService } from '@classic-homes/offline-core/cache';Default Expirations
- Files: 7 days
- Drafts: 24 hours
- Cache: 5 minutes
These can be overridden per-operation.
Error Handling
import { OfflineStorageError } from '@classic-homes/offline-core';
try {
await storage.files.storeFile(largeFile, options);
} catch (error) {
if (error instanceof OfflineStorageError) {
if (error.code === 'STORAGE_FULL') {
// Handle quota exceeded
}
}
}Testing
npm test # Run tests
npm run test:watch # Watch mode
npm run test:coverage # With coverageLicense
MIT
