Delete store definition

This commit is contained in:
omaroughriss 2025-11-27 16:43:49 +01:00
parent 71d0d14095
commit f74fcabec7

View File

@ -1,467 +1,429 @@
import Services from './service'; import Services from './service';
export class Database { export class Database {
private static instance: Database; private static instance: Database;
private db: IDBDatabase | null = null; private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private dbName: string = '4nk'; private serviceWorkerCheckIntervalId: number | null = null;
private dbVersion: number = 1; private indexedDBWorker: Worker | null = null;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null; private messageIdCounter: number = 0;
private messageChannel: MessageChannel | null = null; private pendingMessages: Map<number, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map();
private messageChannelForGet: MessageChannel | null = null;
private serviceWorkerCheckIntervalId: number | null = null; // ============================================
private storeDefinitions = { // INITIALIZATION & SINGLETON
AnkLabels: { // ============================================
name: 'labels',
options: { keyPath: 'emoji' }, private constructor() {
indices: [], this.initIndexedDBWorker();
}, this.initServiceWorker();
AnkWallet: { }
name: 'wallet',
options: { keyPath: 'pre_id' }, public static async getInstance(): Promise<Database> {
indices: [], if (!Database.instance) {
}, Database.instance = new Database();
AnkProcess: { await Database.instance.init();
name: 'processes', }
options: {}, return Database.instance;
indices: [], }
},
AnkSharedSecrets: { // Initialize the database
name: 'shared_secrets', private async init(): Promise<void> {
options: {}, return new Promise((resolve, reject) => {
indices: [], const request = indexedDB.open(this.dbName, this.dbVersion);
},
AnkUnconfirmedSecrets: { request.onupgradeneeded = () => {
name: 'unconfirmed_secrets', const db = request.result;
options: { autoIncrement: true },
indices: [], Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
}, if (!db.objectStoreNames.contains(name)) {
AnkPendingDiffs: { let store = db.createObjectStore(name, options as IDBObjectStoreParameters);
name: 'diffs',
options: { keyPath: 'value_commitment' }, indices.forEach(({ name, keyPath, options }) => {
indices: [ store.createIndex(name, keyPath, options);
{ name: 'byStateId', keyPath: 'state_id', options: { unique: false } }, });
{ name: 'byNeedValidation', keyPath: 'need_validation', options: { unique: false } }, }
{ name: 'byStatus', keyPath: 'validation_status', options: { unique: false } }, });
], };
},
AnkData: { request.onsuccess = async () => {
name: 'data', this.db = request.result;
options: {}, resolve();
indices: [], };
},
}; request.onerror = () => {
console.error('Database error:', request.error);
// Private constructor to prevent direct instantiation from outside reject(request.error);
private constructor() {} };
});
// Method to access the singleton instance of Database }
public static async getInstance(): Promise<Database> {
if (!Database.instance) { public async getDb(): Promise<IDBDatabase> {
Database.instance = new Database(); if (!this.db) {
await Database.instance.init(); await this.init();
} }
return Database.instance; return this.db!;
} }
// Initialize the database public getStoreList(): { [key: string]: string } {
private async init(): Promise<void> { const objectList: { [key: string]: string } = {};
return new Promise((resolve, reject) => { Object.keys(this.storeDefinitions).forEach((key) => {
const request = indexedDB.open(this.dbName, this.dbVersion); objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
});
request.onupgradeneeded = () => { return objectList;
const db = request.result; }
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => { public async registerServiceWorker(path: string) {
if (!db.objectStoreNames.contains(name)) { if (!('serviceWorker' in navigator)) return;
let store = db.createObjectStore(name, options as IDBObjectStoreParameters); console.log('[Database] Initialisation du Service Worker sur :', path);
indices.forEach(({ name, keyPath, options }) => { try {
store.createIndex(name, keyPath, options); // 1. NETTOYAGE DES ANCIENS WORKERS (ZOMBIES)
}); const registrations = await navigator.serviceWorker.getRegistrations();
}
}); for (const registration of registrations) {
}; const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL;
const scope = registration.scope;
request.onsuccess = async () => {
this.db = request.result; // On détecte spécifiquement l'ancien dossier qui pose problème
resolve(); // L'erreur mentionne : scope ('.../src/service-workers/')
}; if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) {
console.warn(`[Database] 🚨 ANCIEN Service Worker détecté (${scope}). Suppression immédiate...`);
request.onerror = () => { await registration.unregister();
console.error('Database error:', request.error); // On continue la boucle, ne pas retourner ici, il faut installer le nouveau après
reject(request.error); }
}; }
});
} // 2. INSTALLATION DU NOUVEAU WORKER (PROPRE)
// On vérifie s'il est déjà installé à la BONNE adresse
public async getDb(): Promise<IDBDatabase> { const existingValidWorker = registrations.find((r) => {
if (!this.db) { const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL;
await this.init(); // On compare la fin de l'URL pour éviter les soucis http/https/localhost
} return url && url.endsWith(path.replace(/^\//, ''));
return this.db!; });
}
if (!existingValidWorker) {
public getStoreList(): { [key: string]: string } { console.log('[Database] Enregistrement du nouveau Service Worker...');
const objectList: { [key: string]: string } = {}; this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' });
Object.keys(this.storeDefinitions).forEach((key) => { } else {
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name; console.log('[Database] Service Worker déjà actif et valide.');
}); this.serviceWorkerRegistration = existingValidWorker;
return objectList; await this.serviceWorkerRegistration.update();
} }
// Set up listeners
public async registerServiceWorker(path: string) { navigator.serviceWorker.addEventListener('message', async (event) => {
if (!('serviceWorker' in navigator)) return; // console.log('Received message from service worker:', event.data);
console.log('[Database] Initialisation du Service Worker sur :', path); await this.handleServiceWorkerMessage(event.data);
});
try {
// 1. NETTOYAGE DES ANCIENS WORKERS (ZOMBIES) // Periodic check
const registrations = await navigator.serviceWorker.getRegistrations(); if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId);
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
for (const registration of registrations) { const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL; const service = await Services.getInstance();
const scope = registration.scope; const payload = await service.getMyProcesses();
if (payload && payload.length != 0) {
// On détecte spécifiquement l'ancien dossier qui pose problème activeWorker?.postMessage({ type: 'SCAN', payload });
// L'erreur mentionne : scope ('.../src/service-workers/') }
if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) { }, 5000);
console.warn(`[Database] 🚨 ANCIEN Service Worker détecté (${scope}). Suppression immédiate...`); } catch (error) {
await registration.unregister(); console.error('[Database] 💥 Erreur critique Service Worker:', error);
// On continue la boucle, ne pas retourner ici, il faut installer le nouveau après }
} }
}
// Helper function to wait for service worker activation
// 2. INSTALLATION DU NOUVEAU WORKER (PROPRE) private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
// On vérifie s'il est déjà installé à la BONNE adresse return new Promise((resolve) => {
const existingValidWorker = registrations.find((r) => { if (registration.active) {
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL; resolve(registration.active);
// On compare la fin de l'URL pour éviter les soucis http/https/localhost } else {
return url && url.endsWith(path.replace(/^\//, '')); const listener = () => {
}); if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener);
if (!existingValidWorker) { resolve(registration.active);
console.log('[Database] Enregistrement du nouveau Service Worker...'); }
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' }); };
} else { navigator.serviceWorker.addEventListener('controllerchange', listener);
console.log('[Database] Service Worker déjà actif et valide.'); }
this.serviceWorkerRegistration = existingValidWorker; });
await this.serviceWorkerRegistration.update(); }
}
// Set up listeners private async checkForUpdates() {
navigator.serviceWorker.addEventListener('message', async (event) => { if (this.serviceWorkerRegistration) {
// console.log('Received message from service worker:', event.data); // Check for updates to the service worker
await this.handleServiceWorkerMessage(event.data); try {
}); await this.serviceWorkerRegistration.update();
// Periodic check // If there's a new worker waiting, activate it immediately
if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId); if (this.serviceWorkerRegistration.waiting) {
this.serviceWorkerCheckIntervalId = window.setInterval(async () => { this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!)); }
const service = await Services.getInstance(); } catch (error) {
const payload = await service.getMyProcesses(); console.error('Error checking for service worker updates:', error);
if (payload && payload.length != 0) { }
activeWorker?.postMessage({ type: 'SCAN', payload }); }
} }
}, 5000);
} catch (error) { private async handleServiceWorkerMessage(message: any) {
console.error('[Database] 💥 Erreur critique Service Worker:', error); switch (message.type) {
} case 'TO_DOWNLOAD':
} await this.handleDownloadList(message.data);
break;
// Helper function to wait for service worker activation default:
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> { console.warn('Unknown message type received from service worker:', message);
return new Promise((resolve) => { }
if (registration.active) { }
resolve(registration.active);
} else { private async handleDownloadList(downloadList: string[]): Promise<void> {
const listener = () => { // Download the missing data
if (registration.active) { let requestedStateId: string[] = [];
navigator.serviceWorker.removeEventListener('controllerchange', listener); const service = await Services.getInstance();
resolve(registration.active); for (const hash of downloadList) {
} const diff = await service.getDiffByValue(hash);
}; if (!diff) {
navigator.serviceWorker.addEventListener('controllerchange', listener); // This should never happen
} console.warn(`Missing a diff for hash ${hash}`);
}); continue;
} }
const processId = diff.process_id;
private async checkForUpdates() { const stateId = diff.state_id;
if (this.serviceWorkerRegistration) { const roles = diff.roles;
// Check for updates to the service worker try {
try { const valueBytes = await service.fetchValueFromStorage(hash);
await this.serviceWorkerRegistration.update(); if (valueBytes) {
// Save data to db
// If there's a new worker waiting, activate it immediately const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
if (this.serviceWorkerRegistration.waiting) { await service.saveBlobToDb(hash, blob);
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); document.dispatchEvent(
} new CustomEvent('newDataReceived', {
} catch (error) { detail: {
console.error('Error checking for service worker updates:', error); processId,
} stateId,
} hash,
} },
}),
private async handleServiceWorkerMessage(message: any) { );
switch (message.type) { } else {
case 'TO_DOWNLOAD': // We first request the data from managers
await this.handleDownloadList(message.data); console.log('Request data from managers of the process');
break; // get the diff from db
default: if (!requestedStateId.includes(stateId)) {
console.warn('Unknown message type received from service worker:', message); await service.requestDataFromPeers(processId, [stateId], [roles]);
} requestedStateId.push(stateId);
} }
}
private async handleDownloadList(downloadList: string[]): Promise<void> { } catch (e) {
// Download the missing data console.error(e);
let requestedStateId: string[] = []; }
const service = await Services.getInstance(); }
for (const hash of downloadList) { }
const diff = await service.getDiffByValue(hash);
if (!diff) { private handleAddObjectResponse = async (event: MessageEvent) => {
// This should never happen const data = event.data;
console.warn(`Missing a diff for hash ${hash}`); console.log('Received response from service worker (ADD_OBJECT):', data);
continue; const service = await Services.getInstance();
} if (data.type === 'NOTIFICATIONS') {
const processId = diff.process_id; service.setNotifications(data.data);
const stateId = diff.state_id; } else if (data.type === 'TO_DOWNLOAD') {
const roles = diff.roles; console.log(`Received missing data ${data}`);
try { // Download the missing data
const valueBytes = await service.fetchValueFromStorage(hash); let requestedStateId: string[] = [];
if (valueBytes) { for (const hash of data.data) {
// Save data to db try {
const blob = new Blob([valueBytes], { type: 'application/octet-stream' }); const valueBytes = await service.fetchValueFromStorage(hash);
await service.saveBlobToDb(hash, blob); if (valueBytes) {
document.dispatchEvent( // Save data to db
new CustomEvent('newDataReceived', { const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
detail: { await service.saveBlobToDb(hash, blob);
processId, } else {
stateId, // We first request the data from managers
hash, console.log('Request data from managers of the process');
}, // get the diff from db
}), const diff = await service.getDiffByValue(hash);
); if (diff === null) {
} else { continue;
// We first request the data from managers }
console.log('Request data from managers of the process'); const processId = diff!.process_id;
// get the diff from db const stateId = diff!.state_id;
if (!requestedStateId.includes(stateId)) { const roles = diff!.roles;
await service.requestDataFromPeers(processId, [stateId], [roles]); if (!requestedStateId.includes(stateId)) {
requestedStateId.push(stateId); await service.requestDataFromPeers(processId, [stateId], [roles]);
} requestedStateId.push(stateId);
} }
} catch (e) { }
console.error(e); } catch (e) {
} console.error(e);
} }
} }
}
private handleAddObjectResponse = async (event: MessageEvent) => { };
const data = event.data;
console.log('Received response from service worker (ADD_OBJECT):', data); private handleGetObjectResponse = (event: MessageEvent) => {
const service = await Services.getInstance(); console.log('Received response from service worker (GET_OBJECT):', event.data);
if (data.type === 'NOTIFICATIONS') { };
service.setNotifications(data.data);
} else if (data.type === 'TO_DOWNLOAD') { public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
console.log(`Received missing data ${data}`); return new Promise(async (resolve, reject) => {
// Download the missing data // Check if the service worker is active
let requestedStateId: string[] = []; if (!this.serviceWorkerRegistration) {
for (const hash of data.data) { // console.warn('Service worker registration is not ready. Waiting...');
try { this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
const valueBytes = await service.fetchValueFromStorage(hash); }
if (valueBytes) {
// Save data to db const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
await service.saveBlobToDb(hash, blob); // Create a message channel for communication
} else { const messageChannel = new MessageChannel();
// We first request the data from managers
console.log('Request data from managers of the process'); // Handle the response from the service worker
// get the diff from db messageChannel.port1.onmessage = (event) => {
const diff = await service.getDiffByValue(hash); if (event.data.status === 'success') {
if (diff === null) { resolve();
continue; } else {
} const error = event.data.message;
const processId = diff!.process_id; reject(new Error(error || 'Unknown error occurred while adding object'));
const stateId = diff!.state_id; }
const roles = diff!.roles; };
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]); // Send the add object request to the service worker
requestedStateId.push(stateId); try {
} activeWorker?.postMessage(
} {
} catch (e) { type: 'ADD_OBJECT',
console.error(e); payload,
} },
} [messageChannel.port2],
} );
}; } catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`));
private handleGetObjectResponse = (event: MessageEvent) => { }
console.log('Received response from service worker (GET_OBJECT):', event.data); });
}; }
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> { public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// Check if the service worker is active if (!this.serviceWorkerRegistration) {
if (!this.serviceWorkerRegistration) { this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
// console.warn('Service worker registration is not ready. Waiting...'); }
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
} const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
const messageChannel = new MessageChannel();
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
messageChannel.port1.onmessage = (event) => {
// Create a message channel for communication if (event.data.status === 'success') {
const messageChannel = new MessageChannel(); resolve();
} else {
// Handle the response from the service worker const error = event.data.message;
messageChannel.port1.onmessage = (event) => { reject(new Error(error || 'Unknown error occurred while adding objects'));
if (event.data.status === 'success') { }
resolve(); };
} else {
const error = event.data.message; try {
reject(new Error(error || 'Unknown error occurred while adding object')); activeWorker?.postMessage(
} {
}; type: 'BATCH_WRITING',
payload,
// Send the add object request to the service worker },
try { [messageChannel.port2],
activeWorker?.postMessage( );
{ } catch (error) {
type: 'ADD_OBJECT', reject(new Error(`Failed to send message to service worker: ${error}`));
payload, }
}, });
[messageChannel.port2], }
);
} catch (error) { public async getObject(storeName: string, key: string): Promise<any | null> {
reject(new Error(`Failed to send message to service worker: ${error}`)); const db = await this.getDb();
} const tx = db.transaction(storeName, 'readonly');
}); const store = tx.objectStore(storeName);
} const result = await new Promise((resolve, reject) => {
const getRequest = store.get(key);
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> { getRequest.onsuccess = () => resolve(getRequest.result);
return new Promise(async (resolve, reject) => { getRequest.onerror = () => reject(getRequest.error);
if (!this.serviceWorkerRegistration) { });
this.serviceWorkerRegistration = await navigator.serviceWorker.ready; return result ?? null; // Convert undefined to null
} }
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration); public async dumpStore(storeName: string): Promise<Record<string, any>> {
const messageChannel = new MessageChannel(); const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
messageChannel.port1.onmessage = (event) => { const store = tx.objectStore(storeName);
if (event.data.status === 'success') {
resolve(); try {
} else { return new Promise((resolve, reject) => {
const error = event.data.message; const result: Record<string, any> = {};
reject(new Error(error || 'Unknown error occurred while adding objects')); const cursor = store.openCursor();
}
}; cursor.onsuccess = (event) => {
const request = event.target as IDBRequest<IDBCursorWithValue | null>;
try { const cursor = request.result;
activeWorker?.postMessage( if (cursor) {
{ result[cursor.key as string] = cursor.value;
type: 'BATCH_WRITING', cursor.continue();
payload, } else {
}, resolve(result);
[messageChannel.port2], }
); };
} catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`)); cursor.onerror = () => {
} reject(cursor.error);
}); };
} });
} catch (error) {
public async getObject(storeName: string, key: string): Promise<any | null> { console.error('Error fetching data from IndexedDB:', error);
const db = await this.getDb(); throw error;
const tx = db.transaction(storeName, 'readonly'); }
const store = tx.objectStore(storeName); }
const result = await new Promise((resolve, reject) => {
const getRequest = store.get(key); public async deleteObject(storeName: string, key: string): Promise<void> {
getRequest.onsuccess = () => resolve(getRequest.result); const db = await this.getDb();
getRequest.onerror = () => reject(getRequest.error); const tx = db.transaction(storeName, 'readwrite');
}); const store = tx.objectStore(storeName);
return result ?? null; // Convert undefined to null try {
} await new Promise((resolve, reject) => {
const getRequest = store.delete(key);
public async dumpStore(storeName: string): Promise<Record<string, any>> { getRequest.onsuccess = () => resolve(getRequest.result);
const db = await this.getDb(); getRequest.onerror = () => reject(getRequest.error);
const tx = db.transaction(storeName, 'readonly'); });
const store = tx.objectStore(storeName); } catch (e) {
throw e;
try { }
return new Promise((resolve, reject) => { }
const result: Record<string, any> = {};
const cursor = store.openCursor(); public async clearStore(storeName: string): Promise<void> {
const db = await this.getDb();
cursor.onsuccess = (event) => { const tx = db.transaction(storeName, 'readwrite');
const request = event.target as IDBRequest<IDBCursorWithValue | null>; const store = tx.objectStore(storeName);
const cursor = request.result; try {
if (cursor) { await new Promise((resolve, reject) => {
result[cursor.key as string] = cursor.value; const clearRequest = store.clear();
cursor.continue(); clearRequest.onsuccess = () => resolve(clearRequest.result);
} else { clearRequest.onerror = () => reject(clearRequest.error);
resolve(result); });
} } catch (e) {
}; throw e;
}
cursor.onerror = () => { }
reject(cursor.error);
}; // Request a store by index
}); public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
} catch (error) { const db = await this.getDb();
console.error('Error fetching data from IndexedDB:', error); const tx = db.transaction(storeName, 'readonly');
throw error; const store = tx.objectStore(storeName);
} const index = store.index(indexName);
}
try {
public async deleteObject(storeName: string, key: string): Promise<void> { return new Promise((resolve, reject) => {
const db = await this.getDb(); const getAllRequest = index.getAll(request);
const tx = db.transaction(storeName, 'readwrite'); getAllRequest.onsuccess = () => {
const store = tx.objectStore(storeName); const allItems = getAllRequest.result;
try { const filtered = allItems.filter((item) => item.state_id === request);
await new Promise((resolve, reject) => { resolve(filtered);
const getRequest = store.delete(key); };
getRequest.onsuccess = () => resolve(getRequest.result); getAllRequest.onerror = () => reject(getAllRequest.error);
getRequest.onerror = () => reject(getRequest.error); });
}); } catch (e) {
} catch (e) { throw e;
throw e; }
} }
} }
public async clearStore(storeName: string): Promise<void> { export default Database;
const db = await this.getDb();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
try {
await new Promise((resolve, reject) => {
const clearRequest = store.clear();
clearRequest.onsuccess = () => resolve(clearRequest.result);
clearRequest.onerror = () => reject(clearRequest.error);
});
} catch (e) {
throw e;
}
}
// Request a store by index
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const index = store.index(indexName);
try {
return new Promise((resolve, reject) => {
const getAllRequest = index.getAll(request);
getAllRequest.onsuccess = () => {
const allItems = getAllRequest.result;
const filtered = allItems.filter((item) => item.state_id === request);
resolve(filtered);
};
getAllRequest.onerror = () => reject(getAllRequest.error);
});
} catch (e) {
throw e;
}
}
}
export default Database;