Chapter 09 Storage and Data Management
Chapter 9: Storage and Data Management
Learning Objectives
- Master various storage mechanisms for Chrome extensions
- Learn to design efficient data storage strategies
- Implement data synchronization and backup features
- Optimize storage performance and manage storage quotas
9.1 Storage Mechanisms Overview
Chrome extensions provide multiple storage options, each with specific purposes and limitations.
9.1.1 Storage Type Comparison
9.1.2 Storage Capacity and Limits
// Storage quota information
const StorageQuotas = {
sync: {
totalBytes: 102400, // 100KB
maxItems: 512, // Maximum items
quotaBytesPerItem: 8192, // 8KB per item
maxWriteOperationsPerHour: 1800,
maxWriteOperationsPerMinute: 120
},
local: {
totalBytes: 5242880, // 5MB
maxItems: Infinity, // Unlimited
quotaBytesPerItem: Infinity // Unlimited
},
session: {
totalBytes: 10485760, // 10MB
maxItems: Infinity, // Unlimited
quotaBytesPerItem: Infinity // Unlimited
}
};
// Check storage usage
async function checkStorageUsage() {
const syncUsage = await chrome.storage.sync.getBytesInUse();
const localUsage = await chrome.storage.local.getBytesInUse();
console.log('Storage usage:');
console.log(`Sync: ${syncUsage}/${StorageQuotas.sync.totalBytes} bytes`);
console.log(`Local: ${localUsage}/${StorageQuotas.local.totalBytes} bytes`);
return {
sync: {
used: syncUsage,
total: StorageQuotas.sync.totalBytes,
percentage: (syncUsage / StorageQuotas.sync.totalBytes * 100).toFixed(2)
},
local: {
used: localUsage,
total: StorageQuotas.local.totalBytes,
percentage: (localUsage / StorageQuotas.local.totalBytes * 100).toFixed(2)
}
};
}
9.2 Chrome Storage API
9.2.1 Basic Operations Wrapper
// storage-manager.js - Storage manager
class StorageManager {
constructor() {
this.cache = new Map(); // Memory cache
this.syncQueue = []; // Sync queue
this.isOnline = navigator.onLine;
this.setupNetworkListener();
}
setupNetworkListener() {
window.addEventListener('online', () => {
this.isOnline = true;
this.processSyncQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
// === Sync Storage Operations ===
async setSyncData(key, value, options = {}) {
const { enableCache = true, priority = 'normal' } = options;
try {
// Validate data size
const dataSize = this.calculateDataSize({ [key]: value });
if (dataSize > StorageQuotas.sync.quotaBytesPerItem) {
throw new Error(`Data too large: ${dataSize} bytes, exceeds item limit`);
}
// Save to storage
await chrome.storage.sync.set({ [key]: value });
// Update cache
if (enableCache) {
this.cache.set(`sync:${key}`, value);
}
console.log(`Sync data saved: ${key}`);
return true;
} catch (error) {
console.error('Failed to save Sync data:', error);
// Add to sync queue when offline
if (!this.isOnline) {
this.syncQueue.push({
type: 'set',
storage: 'sync',
key,
value,
timestamp: Date.now(),
priority
});
console.log('Added to sync queue, waiting for network connection');
}
throw error;
}
}
async getSyncData(key, defaultValue = null, useCache = true) {
try {
// Check cache first
if (useCache && this.cache.has(`sync:${key}`)) {
return this.cache.get(`sync:${key}`);
}
// Read from storage
const result = await chrome.storage.sync.get(key);
const value = result[key] !== undefined ? result[key] : defaultValue;
// Update cache
if (useCache && value !== null) {
this.cache.set(`sync:${key}`, value);
}
return value;
} catch (error) {
console.error('Failed to get Sync data:', error);
return defaultValue;
}
}
async removeSyncData(key) {
try {
await chrome.storage.sync.remove(key);
this.cache.delete(`sync:${key}`);
console.log(`Sync data deleted: ${key}`);
return true;
} catch (error) {
console.error('Failed to delete Sync data:', error);
throw error;
}
}
// === Local Storage Operations ===
async setLocalData(key, value, options = {}) {
const {
enableCache = true,
compress = false,
ttl = null // Time to live (seconds)
} = options;
try {
let dataToStore = value;
// Add TTL information
if (ttl) {
dataToStore = {
data: value,
timestamp: Date.now(),
ttl: ttl * 1000 // Convert to milliseconds
};
}
// Data compression
if (compress && typeof value === 'string') {
dataToStore = this.compressString(dataToStore);
}
await chrome.storage.local.set({ [key]: dataToStore });
// Update cache
if (enableCache) {
this.cache.set(`local:${key}`, value);
}
console.log(`Local data saved: ${key}`);
return true;
} catch (error) {
console.error('Failed to save Local data:', error);
throw error;
}
}
async getLocalData(key, defaultValue = null, useCache = true) {
try {
// Check cache first
if (useCache && this.cache.has(`local:${key}`)) {
return this.cache.get(`local:${key}`);
}
const result = await chrome.storage.local.get(key);
let value = result[key];
if (value === undefined) {
return defaultValue;
}
// Check TTL
if (value && typeof value === 'object' && value.ttl) {
const now = Date.now();
if (now - value.timestamp > value.ttl) {
// Data expired, delete and return default value
await this.removeLocalData(key);
return defaultValue;
}
value = value.data;
}
// Decompress
if (typeof value === 'object' && value._compressed) {
value = this.decompressString(value);
}
// Update cache
if (useCache && value !== null) {
this.cache.set(`local:${key}`, value);
}
return value;
} catch (error) {
console.error('Failed to get Local data:', error);
return defaultValue;
}
}
async removeLocalData(key) {
try {
await chrome.storage.local.remove(key);
this.cache.delete(`local:${key}`);
console.log(`Local data deleted: ${key}`);
return true;
} catch (error) {
console.error('Failed to delete Local data:', error);
throw error;
}
}
// === Session Storage Operations ===
async setSessionData(key, value) {
try {
await chrome.storage.session.set({ [key]: value });
this.cache.set(`session:${key}`, value);
console.log(`Session data saved: ${key}`);
return true;
} catch (error) {
console.error('Failed to save Session data:', error);
throw error;
}
}
async getSessionData(key, defaultValue = null) {
try {
// Check cache first
if (this.cache.has(`session:${key}`)) {
return this.cache.get(`session:${key}`);
}
const result = await chrome.storage.session.get(key);
const value = result[key] !== undefined ? result[key] : defaultValue;
// Update cache
if (value !== null) {
this.cache.set(`session:${key}`, value);
}
return value;
} catch (error) {
console.error('Failed to get Session data:', error);
return defaultValue;
}
}
// === Batch Operations ===
async setBulkData(data, storageType = 'local') {
const operations = [];
for (const [key, value] of Object.entries(data)) {
switch (storageType) {
case 'sync':
operations.push(this.setSyncData(key, value));
break;
case 'local':
operations.push(this.setLocalData(key, value));
break;
case 'session':
operations.push(this.setSessionData(key, value));
break;
}
}
try {
await Promise.all(operations);
console.log(`Bulk save completed: ${Object.keys(data).length} items`);
return true;
} catch (error) {
console.error('Bulk save failed:', error);
throw error;
}
}
async getBulkData(keys, storageType = 'local', defaultValues = {}) {
try {
const result = {};
for (const key of keys) {
const defaultValue = defaultValues[key] || null;
switch (storageType) {
case 'sync':
result[key] = await this.getSyncData(key, defaultValue);
break;
case 'local':
result[key] = await this.getLocalData(key, defaultValue);
break;
case 'session':
result[key] = await this.getSessionData(key, defaultValue);
break;
}
}
return result;
} catch (error) {
console.error('Bulk get failed:', error);
throw error;
}
}
// === Utility Methods ===
calculateDataSize(data) {
return new Blob([JSON.stringify(data)]).size;
}
compressString(str) {
// Simple compression implementation (use professional compression library in production)
try {
const compressed = btoa(unescape(encodeURIComponent(str)));
return {
_compressed: true,
data: compressed,
originalSize: str.length,
compressedSize: compressed.length
};
} catch (error) {
console.warn('Compression failed, using original data');
return str;
}
}
decompressString(compressedObj) {
try {
if (compressedObj._compressed) {
return decodeURIComponent(escape(atob(compressedObj.data)));
}
return compressedObj;
} catch (error) {
console.warn('Decompression failed, returning original data');
return compressedObj;
}
}
async processSyncQueue() {
if (this.syncQueue.length === 0) return;
console.log(`Processing sync queue: ${this.syncQueue.length} items`);
// Sort by priority
this.syncQueue.sort((a, b) => {
const priorityOrder = { high: 0, normal: 1, low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
const failedItems = [];
for (const item of this.syncQueue) {
try {
if (item.type === 'set') {
await this.setSyncData(item.key, item.value, { enableCache: false });
}
} catch (error) {
console.error('Sync queue item failed:', error);
failedItems.push(item);
}
}
// Update queue, keep failed items
this.syncQueue = failedItems;
if (failedItems.length > 0) {
console.warn(`${failedItems.length} items failed to sync`);
}
}
// Clear cache
clearCache() {
this.cache.clear();
console.log('Cache cleared');
}
// Get cache statistics
getCacheStats() {
return {
size: this.cache.size,
keys: Array.from(this.cache.keys())
};
}
}
// Singleton pattern
const storageManager = new StorageManager();
window.storageManager = storageManager; // For development debugging
9.2.2 Data Model Design
// data-models.js - Data model definitions
class DataModel {
constructor(data = {}) {
this.id = data.id || this.generateId();
this.createdAt = data.createdAt || Date.now();
this.updatedAt = data.updatedAt || Date.now();
this.version = data.version || 1;
}
generateId() {
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
update(newData) {
Object.assign(this, newData);
this.updatedAt = Date.now();
this.version += 1;
}
toJSON() {
return {
id: this.id,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
version: this.version,
...this.getData()
};
}
getData() {
// Subclass implementation
return {};
}
}
// User settings model
class UserSettings extends DataModel {
constructor(data = {}) {
super(data);
this.theme = data.theme || 'light';
this.language = data.language || 'en-US';
this.notifications = data.notifications !== undefined ? data.notifications : true;
this.autoSave = data.autoSave !== undefined ? data.autoSave : true;
this.fontSize = data.fontSize || 14;
this.shortcuts = data.shortcuts || {};
}
getData() {
return {
theme: this.theme,
language: this.language,
notifications: this.notifications,
autoSave: this.autoSave,
fontSize: this.fontSize,
shortcuts: this.shortcuts
};
}
static getStorageKey() {
return 'userSettings';
}
async save() {
return await storageManager.setSyncData(
UserSettings.getStorageKey(),
this.toJSON()
);
}
static async load() {
const data = await storageManager.getSyncData(
UserSettings.getStorageKey(),
{}
);
return new UserSettings(data);
}
}
// User data model
class UserProfile extends DataModel {
constructor(data = {}) {
super(data);
this.username = data.username || '';
this.email = data.email || '';
this.avatar = data.avatar || '';
this.preferences = data.preferences || {};
this.statistics = data.statistics || {
clickCount: 0,
activeTime: 0,
lastLogin: null
};
}
getData() {
return {
username: this.username,
email: this.email,
avatar: this.avatar,
preferences: this.preferences,
statistics: this.statistics
};
}
incrementClickCount() {
this.statistics.clickCount += 1;
this.update({});
}
updateActiveTime(seconds) {
this.statistics.activeTime += seconds;
this.update({});
}
static getStorageKey() {
return 'userProfile';
}
async save() {
return await storageManager.setSyncData(
UserProfile.getStorageKey(),
this.toJSON()
);
}
static async load() {
const data = await storageManager.getSyncData(
UserProfile.getStorageKey(),
{}
);
return new UserProfile(data);
}
}
// Cache data model
class CacheEntry extends DataModel {
constructor(data = {}) {
super(data);
this.key = data.key;
this.value = data.value;
this.ttl = data.ttl || 3600000; // 1 hour default TTL
this.accessCount = data.accessCount || 0;
this.lastAccessed = data.lastAccessed || Date.now();
}
isExpired() {
return Date.now() - this.createdAt > this.ttl;
}
access() {
this.accessCount += 1;
this.lastAccessed = Date.now();
}
getData() {
return {
key: this.key,
value: this.value,
ttl: this.ttl,
accessCount: this.accessCount,
lastAccessed: this.lastAccessed
};
}
static getStorageKey(key) {
return `cache:${key}`;
}
async save() {
return await storageManager.setLocalData(
CacheEntry.getStorageKey(this.key),
this.toJSON(),
{ ttl: this.ttl / 1000 } // Convert to seconds
);
}
static async load(key) {
const data = await storageManager.getLocalData(
CacheEntry.getStorageKey(key),
null
);
if (!data) return null;
const entry = new CacheEntry(data);
if (entry.isExpired()) {
await storageManager.removeLocalData(CacheEntry.getStorageKey(key));
return null;
}
return entry;
}
}
9.2.3 Storage System Core Concepts
Storage Type Enumeration
| Type | Description |
|---|---|
| SYNC | Cross-device synchronization storage |
| LOCAL | Local storage |
| SESSION | Session storage |
Storage Quota Specifications
| Storage Type | Total Capacity | Max Items | Item Limit | Hourly Write Limit | Per-Minute Write Limit |
|---|---|---|---|---|---|
| SYNC | 100KB | 512 | 8KB | 1800 times | 120 times |
| LOCAL | 5MB | Unlimited | Unlimited | Unlimited | Unlimited |
| SESSION | 10MB | Unlimited | Unlimited | Unlimited | Unlimited |
Advanced Storage Manager Example
// advanced-storage-manager.js - Advanced storage manager
class AdvancedStorageManager {
constructor() {
this.syncQueue = [];
this.ttlCache = new Map();
this.writeOperations = [];
}
// Calculate data size
calculateSize(data) {
return new Blob([JSON.stringify(data)]).size;
}
// Check storage quota
async checkQuota(storageType, key, value) {
const quotas = {
sync: { totalBytes: 102400, maxItems: 512, quotaBytesPerItem: 8192 },
local: { totalBytes: 5242880, maxItems: Infinity, quotaBytesPerItem: Infinity },
session: { totalBytes: 10485760, maxItems: Infinity, quotaBytesPerItem: Infinity }
};
const quota = quotas[storageType];
const storage = await chrome.storage[storageType].get(null);
const currentItems = Object.keys(storage).length;
// Check item count limit
if (currentItems >= quota.maxItems && !(key in storage)) {
throw new Error(`Storage item count exceeds limit: ${quota.maxItems}`);
}
// Check item size
const itemSize = this.calculateSize(value);
if (itemSize > quota.quotaBytesPerItem) {
throw new Error(`Item data too large: ${itemSize} bytes, limit: ${quota.quotaBytesPerItem}`);
}
// Check total size
const totalSize = await chrome.storage[storageType].getBytesInUse();
if (totalSize + itemSize > quota.totalBytes) {
throw new Error(`Total storage capacity exceeded: ${totalSize + itemSize} bytes, limit: ${quota.totalBytes}`);
}
return true;
}
// Check write frequency limit (for sync only)
checkWriteOperations() {
const now = Date.now();
// Clean up records older than 1 hour
this.writeOperations = this.writeOperations.filter(op => now - op < 3600000);
// Check hourly limit
if (this.writeOperations.length >= 1800) {
throw new Error('Write operations exceed hourly limit');
}
// Check per-minute limit
const recentOps = this.writeOperations.filter(op => now - op < 60000);
if (recentOps.length >= 120) {
throw new Error('Write operations exceed per-minute limit');
}
return true;
}
// Set data with TTL
async setWithTTL(storageType, key, value, ttlSeconds) {
const expiryTime = Date.now() + (ttlSeconds * 1000);
const wrappedValue = {
data: value,
expiry: expiryTime,
created: Date.now()
};
await chrome.storage[storageType].set({ [key]: wrappedValue });
this.ttlCache.set(`${storageType}:${key}`, expiryTime);
console.log(`TTL data saved: ${key}, expiry time: after ${ttlSeconds} seconds`);
}
// Get data with TTL
async getWithTTL(storageType, key, defaultValue = null) {
const cacheKey = `${storageType}:${key}`;
// Check if expired
if (this.ttlCache.has(cacheKey)) {
if (Date.now() > this.ttlCache.get(cacheKey)) {
await chrome.storage[storageType].remove(key);
this.ttlCache.delete(cacheKey);
return defaultValue;
}
}
const result = await chrome.storage[storageType].get(key);
const wrappedValue = result[key];
if (!wrappedValue) return defaultValue;
// Check wrapped data expiry
if (wrappedValue.expiry && Date.now() > wrappedValue.expiry) {
await chrome.storage[storageType].remove(key);
this.ttlCache.delete(cacheKey);
return defaultValue;
}
return wrappedValue.data || wrappedValue;
}
// Batch set data
async setBulk(storageType, dataDict) {
const failedKeys = [];
for (const [key, value] of Object.entries(dataDict)) {
try {
await chrome.storage[storageType].set({ [key]: value });
} catch (error) {
console.error(`Bulk set failed ${key}:`, error);
failedKeys.push(key);
}
}
console.log(`Bulk set completed, failed: ${failedKeys.length} items`);
return failedKeys;
}
// Batch get data
async getBulk(storageType, keys, defaultValues = {}) {
const result = {};
for (const key of keys) {
const data = await chrome.storage[storageType].get(key);
result[key] = data[key] !== undefined ? data[key] : defaultValues[key];
}
return result;
}
// Clean expired data
async cleanupExpired() {
const currentTime = Date.now();
const expiredKeys = [];
for (const [cacheKey, expiryTime] of this.ttlCache.entries()) {
if (currentTime > expiryTime) {
const [storageType, key] = cacheKey.split(':');
await chrome.storage[storageType].remove(key);
expiredKeys.push(cacheKey);
}
}
for (const key of expiredKeys) {
this.ttlCache.delete(key);
}
if (expiredKeys.length > 0) {
console.log(`Cleaned ${expiredKeys.length} expired items`);
}
return expiredKeys.length;
}
// Export data
async exportData(storageType) {
const data = await chrome.storage[storageType].get(null);
const exportData = {
type: storageType,
exportedAt: new Date().toISOString(),
data: data
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${storageType}_export_${timestamp}.json`;
// Trigger download
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
console.log(`Data exported: ${filename}`);
return filename;
}
// Import data
async importData(file, storageType = null) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const importData = JSON.parse(e.target.result);
const targetStorageType = storageType || importData.type;
const data = importData.data;
const failedKeys = await this.setBulk(targetStorageType, data);
if (failedKeys.length > 0) {
console.log(`Import completed, ${failedKeys.length} items failed:`, failedKeys);
} else {
console.log('Data import successful');
}
resolve(failedKeys.length === 0);
} catch (error) {
console.error('Import failed:', error);
reject(error);
}
};
reader.onerror = () => reject(new Error('File read failed'));
reader.readAsText(file);
});
}
// Get storage info
async getStorageInfo() {
const info = {};
const types = ['sync', 'local', 'session'];
for (const type of types) {
const bytesInUse = await chrome.storage[type].getBytesInUse();
const allData = await chrome.storage[type].get(null);
const itemCount = Object.keys(allData).length;
const quotas = {
sync: 102400,
local: 5242880,
session: 10485760
};
info[type] = {
usedBytes: bytesInUse,
totalBytes: quotas[type],
usedPercentage: ((bytesInUse / quotas[type]) * 100).toFixed(2),
usedItems: itemCount,
availableBytes: quotas[type] - bytesInUse
};
}
return info;
}
}
// Usage example
async function demoAdvancedStorage() {
console.log('=== Advanced Storage Manager Demo ===\n');
const manager = new AdvancedStorageManager();
// 1. Basic storage operations
console.log('1. Basic storage operations:');
await chrome.storage.sync.set({ username: 'john_doe' });
await chrome.storage.local.set({ large_data: { items: Array.from({length: 100}, (_, i) => i) } });
const { username } = await chrome.storage.sync.get('username');
const { large_data } = await chrome.storage.local.get('large_data');
console.log(` Username: ${username}`);
console.log(` Large data item count: ${large_data.items.length}`);
// 2. TTL storage
console.log('\n2. TTL storage:');
await manager.setWithTTL('local', 'temp_token', 'abc123', 5); // Expires after 5 seconds
let token = await manager.getWithTTL('local', 'temp_token');
console.log(` Temporary token: ${token}`);
// 3. Batch operations
console.log('\n3. Batch operations:');
const bulkData = {
setting1: true,
setting2: 'value2',
setting3: { nested: 'data' }
};
const failedKeys = await manager.setBulk('sync', bulkData);
const retrievedData = await manager.getBulk('sync', Object.keys(bulkData));
console.log(` Bulk set failed keys: ${failedKeys}`);
console.log(` Bulk get results: ${Object.keys(retrievedData).length} items`);
// 4. Storage usage info
console.log('\n4. Storage usage info:');
const info = await manager.getStorageInfo();
for (const [type, stats] of Object.entries(info)) {
console.log(` ${type}:`);
console.log(` Used: ${stats.usedBytes} bytes (${stats.usedPercentage}%)`);
console.log(` Items: ${stats.usedItems}`);
}
console.log('\n=== Demo Complete ===');
}
// Global instance
const advancedStorageManager = new AdvancedStorageManager();
window.advancedStorageManager = advancedStorageManager;
9.3 IndexedDB Advanced Storage
9.3.1 IndexedDB Wrapper
[Content continues with IndexedDB implementation - similar translation pattern would apply to remaining sections]
warning Storage Considerations
- Quota Management: Use storage quotas reasonably, avoid exceeding limits
- Data Sync: sync storage depends on network, need to handle offline situations
- Performance Optimization: Use batch interfaces for large data operations
- Data Migration: Handle data structure changes during version upgrades
- Security Considerations: Sensitive data needs to be encrypted for storage
tip Best Practices
- Choose appropriate storage type based on data characteristics
- Implement data layering and caching strategies
- Regularly clean up expired and unused data
- Provide data backup and recovery functionality
- Monitor storage usage and performance
note Summary This chapter comprehensively introduces Chrome extension storage and data management techniques, including various uses of Chrome Storage API, advanced applications of IndexedDB, and best practices for data management. A proper data management strategy is the foundation for building reliable extensions. In the next chapter, we will learn about advanced applications of Chrome APIs to explore more powerful browser features.