Files
michaelschiemer/resources/js/modules/api-manager/StorageManager.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

761 lines
25 KiB
JavaScript

// modules/api-manager/StorageManager.js
import { Logger } from '../../core/logger.js';
/**
* Storage APIs Manager - IndexedDB, Cache API, Web Locks, Broadcast Channel
*/
export class StorageManager {
constructor(config = {}) {
this.config = config;
this.dbName = config.dbName || 'AppDatabase';
this.dbVersion = config.dbVersion || 1;
this.db = null;
this.cache = null;
this.channels = new Map();
// Check API support
this.support = {
indexedDB: 'indexedDB' in window,
cacheAPI: 'caches' in window,
webLocks: 'locks' in navigator,
broadcastChannel: 'BroadcastChannel' in window,
localStorage: 'localStorage' in window,
sessionStorage: 'sessionStorage' in window
};
Logger.info('[StorageManager] Initialized with support:', this.support);
// Initialize databases
this.initializeDB();
this.initializeCache();
}
/**
* Initialize IndexedDB
*/
async initializeDB() {
if (!this.support.indexedDB) {
Logger.warn('[StorageManager] IndexedDB not supported');
return;
}
try {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => {
Logger.error('[StorageManager] IndexedDB failed to open');
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create default object stores
if (!db.objectStoreNames.contains('keyValue')) {
const store = db.createObjectStore('keyValue', { keyPath: 'key' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
if (!db.objectStoreNames.contains('cache')) {
db.createObjectStore('cache', { keyPath: 'key' });
}
if (!db.objectStoreNames.contains('files')) {
const fileStore = db.createObjectStore('files', { keyPath: 'id', autoIncrement: true });
fileStore.createIndex('name', 'name', { unique: false });
fileStore.createIndex('type', 'type', { unique: false });
}
Logger.info('[StorageManager] IndexedDB schema updated');
};
request.onsuccess = (event) => {
this.db = event.target.result;
Logger.info('[StorageManager] IndexedDB connected');
};
} catch (error) {
Logger.error('[StorageManager] IndexedDB initialization failed:', error);
}
}
/**
* Initialize Cache API
*/
async initializeCache() {
if (!this.support.cacheAPI) {
Logger.warn('[StorageManager] Cache API not supported');
return;
}
try {
this.cache = await caches.open(this.config.cacheName || 'app-cache-v1');
Logger.info('[StorageManager] Cache API initialized');
} catch (error) {
Logger.error('[StorageManager] Cache API initialization failed:', error);
}
}
/**
* IndexedDB Operations
*/
db = {
// Set key-value data
set: async (key, value, expiration = null) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const data = {
key,
value,
timestamp: Date.now(),
expiration: expiration ? Date.now() + expiration : null
};
const request = store.put(data);
request.onsuccess = () => {
Logger.info(`[StorageManager] DB set: ${key}`);
resolve(data);
};
request.onerror = () => {
Logger.error(`[StorageManager] DB set failed: ${key}`);
reject(request.error);
};
});
},
// Get key-value data
get: async (key) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readonly');
const store = transaction.objectStore('keyValue');
const request = store.get(key);
request.onsuccess = () => {
const result = request.result;
if (!result) {
resolve(null);
return;
}
// Check expiration
if (result.expiration && Date.now() > result.expiration) {
this.db.delete(key); // Auto-cleanup expired data
resolve(null);
return;
}
resolve(result.value);
};
request.onerror = () => {
Logger.error(`[StorageManager] DB get failed: ${key}`);
reject(request.error);
};
});
},
// Delete key
delete: async (key) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const request = store.delete(key);
request.onsuccess = () => {
Logger.info(`[StorageManager] DB deleted: ${key}`);
resolve(true);
};
request.onerror = () => {
reject(request.error);
};
});
},
// Get all keys
keys: async () => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readonly');
const store = transaction.objectStore('keyValue');
const request = store.getAllKeys();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
},
// Clear all data
clear: async () => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const request = store.clear();
request.onsuccess = () => {
Logger.info('[StorageManager] DB cleared');
resolve(true);
};
request.onerror = () => {
reject(request.error);
};
});
},
// Store files/blobs
storeFile: async (name, file, metadata = {}) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['files'], 'readwrite');
const store = transaction.objectStore('files');
const fileData = {
name,
file,
type: file.type,
size: file.size,
timestamp: Date.now(),
metadata
};
const request = store.add(fileData);
request.onsuccess = () => {
Logger.info(`[StorageManager] File stored: ${name}`);
resolve({ id: request.result, ...fileData });
};
request.onerror = () => {
reject(request.error);
};
});
},
// Get file
getFile: async (id) => {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('IndexedDB not available'));
return;
}
const transaction = this.db.transaction(['files'], 'readonly');
const store = transaction.objectStore('files');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
};
/**
* Cache API Operations
*/
cache = {
// Add request/response to cache
add: async (request, response = null) => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
if (response) {
await this.cache.put(request, response);
} else {
await this.cache.add(request);
}
Logger.info(`[StorageManager] Cache add: ${request}`);
} catch (error) {
Logger.error('[StorageManager] Cache add failed:', error);
throw error;
}
},
// Get from cache
get: async (request) => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
const response = await this.cache.match(request);
if (response) {
Logger.info(`[StorageManager] Cache hit: ${request}`);
} else {
Logger.info(`[StorageManager] Cache miss: ${request}`);
}
return response;
} catch (error) {
Logger.error('[StorageManager] Cache get failed:', error);
throw error;
}
},
// Delete from cache
delete: async (request) => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
const success = await this.cache.delete(request);
if (success) {
Logger.info(`[StorageManager] Cache deleted: ${request}`);
}
return success;
} catch (error) {
Logger.error('[StorageManager] Cache delete failed:', error);
throw error;
}
},
// Get all cached requests
keys: async () => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
return await this.cache.keys();
} catch (error) {
Logger.error('[StorageManager] Cache keys failed:', error);
throw error;
}
},
// Clear cache
clear: async () => {
if (!this.cache) {
throw new Error('Cache API not available');
}
try {
const keys = await this.cache.keys();
await Promise.all(keys.map(key => this.cache.delete(key)));
Logger.info('[StorageManager] Cache cleared');
} catch (error) {
Logger.error('[StorageManager] Cache clear failed:', error);
throw error;
}
}
};
/**
* Broadcast Channel for cross-tab communication
*/
channel = {
// Create or get channel
create: (channelName, onMessage = null) => {
if (!this.support.broadcastChannel) {
Logger.warn('[StorageManager] BroadcastChannel not supported');
return null;
}
if (this.channels.has(channelName)) {
return this.channels.get(channelName);
}
const channel = new BroadcastChannel(channelName);
if (onMessage) {
channel.addEventListener('message', onMessage);
}
const channelWrapper = {
name: channelName,
channel,
send: (data) => {
channel.postMessage({
data,
timestamp: Date.now(),
sender: 'current-tab'
});
Logger.info(`[StorageManager] Broadcast sent to ${channelName}`);
},
onMessage: (callback) => {
channel.addEventListener('message', (event) => {
callback(event.data);
});
},
close: () => {
channel.close();
this.channels.delete(channelName);
Logger.info(`[StorageManager] Channel closed: ${channelName}`);
}
};
this.channels.set(channelName, channelWrapper);
Logger.info(`[StorageManager] Channel created: ${channelName}`);
return channelWrapper;
},
// Get existing channel
get: (channelName) => {
return this.channels.get(channelName) || null;
},
// Close channel
close: (channelName) => {
const channel = this.channels.get(channelName);
if (channel) {
channel.close();
}
},
// Close all channels
closeAll: () => {
this.channels.forEach(channel => channel.close());
this.channels.clear();
Logger.info('[StorageManager] All channels closed');
}
};
/**
* Web Locks API for resource locking
*/
locks = {
// Acquire lock
acquire: async (lockName, callback, options = {}) => {
if (!this.support.webLocks) {
Logger.warn('[StorageManager] Web Locks not supported, executing without lock');
return await callback();
}
try {
return await navigator.locks.request(lockName, options, async (lock) => {
Logger.info(`[StorageManager] Lock acquired: ${lockName}`);
const result = await callback(lock);
Logger.info(`[StorageManager] Lock released: ${lockName}`);
return result;
});
} catch (error) {
Logger.error(`[StorageManager] Lock failed: ${lockName}`, error);
throw error;
}
},
// Query locks
query: async () => {
if (!this.support.webLocks) {
return { held: [], pending: [] };
}
try {
return await navigator.locks.query();
} catch (error) {
Logger.error('[StorageManager] Lock query failed:', error);
throw error;
}
}
};
/**
* Local/Session Storage helpers (with JSON support)
*/
local = {
set: (key, value, expiration = null) => {
if (!this.support.localStorage) return false;
try {
const data = {
value,
timestamp: Date.now(),
expiration: expiration ? Date.now() + expiration : null
};
localStorage.setItem(key, JSON.stringify(data));
return true;
} catch (error) {
Logger.error('[StorageManager] localStorage set failed:', error);
return false;
}
},
get: (key) => {
if (!this.support.localStorage) return null;
try {
const item = localStorage.getItem(key);
if (!item) return null;
const data = JSON.parse(item);
// Check expiration
if (data.expiration && Date.now() > data.expiration) {
localStorage.removeItem(key);
return null;
}
return data.value;
} catch (error) {
Logger.error('[StorageManager] localStorage get failed:', error);
return null;
}
},
delete: (key) => {
if (!this.support.localStorage) return false;
localStorage.removeItem(key);
return true;
},
clear: () => {
if (!this.support.localStorage) return false;
localStorage.clear();
return true;
},
keys: () => {
if (!this.support.localStorage) return [];
return Object.keys(localStorage);
}
};
session = {
set: (key, value) => {
if (!this.support.sessionStorage) return false;
try {
sessionStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
Logger.error('[StorageManager] sessionStorage set failed:', error);
return false;
}
},
get: (key) => {
if (!this.support.sessionStorage) return null;
try {
const item = sessionStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
Logger.error('[StorageManager] sessionStorage get failed:', error);
return null;
}
},
delete: (key) => {
if (!this.support.sessionStorage) return false;
sessionStorage.removeItem(key);
return true;
},
clear: () => {
if (!this.support.sessionStorage) return false;
sessionStorage.clear();
return true;
}
};
/**
* Smart caching with automatic expiration
*/
smartCache = {
set: async (key, value, options = {}) => {
const {
storage = 'indexedDB',
expiration = null,
fallback = true
} = options;
try {
if (storage === 'indexedDB' && this.db) {
return await this.db.set(key, value, expiration);
} else if (fallback) {
return this.local.set(key, value, expiration);
}
} catch (error) {
if (fallback) {
return this.local.set(key, value, expiration);
}
throw error;
}
},
get: async (key, options = {}) => {
const { storage = 'indexedDB', fallback = true } = options;
try {
if (storage === 'indexedDB' && this.db) {
return await this.db.get(key);
} else if (fallback) {
return this.local.get(key);
}
} catch (error) {
if (fallback) {
return this.local.get(key);
}
throw error;
}
},
delete: async (key, options = {}) => {
const { storage = 'indexedDB', fallback = true } = options;
try {
if (storage === 'indexedDB' && this.db) {
await this.db.delete(key);
}
if (fallback) {
this.local.delete(key);
}
} catch (error) {
if (fallback) {
this.local.delete(key);
}
throw error;
}
}
};
/**
* Get storage usage statistics
*/
async getStorageUsage() {
const stats = {
quota: 0,
usage: 0,
available: 0,
percentage: 0
};
if ('storage' in navigator && 'estimate' in navigator.storage) {
try {
const estimate = await navigator.storage.estimate();
stats.quota = estimate.quota || 0;
stats.usage = estimate.usage || 0;
stats.available = stats.quota - stats.usage;
stats.percentage = stats.quota > 0 ? Math.round((stats.usage / stats.quota) * 100) : 0;
} catch (error) {
Logger.error('[StorageManager] Storage estimate failed:', error);
}
}
return stats;
}
/**
* Cleanup expired data
*/
async cleanup() {
Logger.info('[StorageManager] Starting cleanup...');
try {
// Cleanup IndexedDB expired entries
if (this.db) {
const transaction = this.db.transaction(['keyValue'], 'readwrite');
const store = transaction.objectStore('keyValue');
const index = store.index('timestamp');
const request = index.openCursor();
let cleaned = 0;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const data = cursor.value;
if (data.expiration && Date.now() > data.expiration) {
cursor.delete();
cleaned++;
}
cursor.continue();
} else {
Logger.info(`[StorageManager] Cleanup completed: ${cleaned} expired entries removed`);
}
};
}
// Cleanup localStorage expired entries
if (this.support.localStorage) {
const keys = Object.keys(localStorage);
let localCleaned = 0;
keys.forEach(key => {
try {
const item = localStorage.getItem(key);
const data = JSON.parse(item);
if (data.expiration && Date.now() > data.expiration) {
localStorage.removeItem(key);
localCleaned++;
}
} catch (error) {
// Ignore non-JSON items
}
});
if (localCleaned > 0) {
Logger.info(`[StorageManager] LocalStorage cleanup: ${localCleaned} expired entries removed`);
}
}
} catch (error) {
Logger.error('[StorageManager] Cleanup failed:', error);
}
}
/**
* Get comprehensive storage status
*/
async getStatus() {
const usage = await this.getStorageUsage();
return {
support: this.support,
usage,
activeChannels: this.channels.size,
dbConnected: !!this.db,
cacheConnected: !!this.cache,
channelNames: Array.from(this.channels.keys())
};
}
}