Compare commits

..

No commits in common. "6fa04317b68d76b21c4cf54eb868d91936b114ca" and "3eeef3fc9af94b586dcab5dac6bfbeb12d638d45" have entirely different histories.

6 changed files with 972 additions and 1124 deletions

View File

@ -1,152 +1,281 @@
const EMPTY32BYTES = String('').padStart(64, '0'); const EMPTY32BYTES = String('').padStart(64, '0');
// ============================================ self.addEventListener('install', (event) => {
// SERVICE WORKER LIFECYCLE event.waitUntil(self.skipWaiting()); // Activate worker immediately
// ============================================ });
self.addEventListener('install', (event) => { self.addEventListener('activate', (event) => {
event.waitUntil(self.skipWaiting()); event.waitUntil(self.clients.claim()); // Become available to all pages
}); });
self.addEventListener('activate', (event) => { // Event listener for messages from clients
event.waitUntil(self.clients.claim()); self.addEventListener('message', async (event) => {
}); const data = event.data;
console.log(data);
// ============================================
// MESSAGE HANDLER if (data.type === 'SCAN') {
// ============================================ try {
const myProcessesId = data.payload;
self.addEventListener('message', async (event) => { if (myProcessesId && myProcessesId.length != 0) {
const data = event.data; const toDownload = await scanMissingData(myProcessesId);
console.log('[Service Worker] Message received:', data.type); if (toDownload.length != 0) {
console.log('Sending TO_DOWNLOAD message');
if (data.type === 'SCAN') { event.source.postMessage({ type: 'TO_DOWNLOAD', data: toDownload});
try { }
const myProcessesId = data.payload; } else {
if (myProcessesId && myProcessesId.length != 0) { event.source.postMessage({ status: 'error', message: 'Empty lists' });
const scanResult = await scanMissingData(myProcessesId, event.source); }
} catch (error) {
if (scanResult.toDownload.length != 0) { event.source.postMessage({ status: 'error', message: error.message });
console.log('[Service Worker] Sending TO_DOWNLOAD message'); }
event.source.postMessage({ type: 'TO_DOWNLOAD', data: scanResult.toDownload }); } else if (data.type === 'ADD_OBJECT') {
} try {
const { storeName, object, key } = data.payload;
if (scanResult.diffsToCreate.length > 0) { const db = await openDatabase();
console.log('[Service Worker] Sending DIFFS_TO_CREATE message'); const tx = db.transaction(storeName, 'readwrite');
event.source.postMessage({ type: 'DIFFS_TO_CREATE', data: scanResult.diffsToCreate }); const store = tx.objectStore(storeName);
}
} else { if (key) {
event.source.postMessage({ status: 'error', message: 'Empty lists' }); await store.put(object, key);
} } else {
} catch (error) { await store.put(object);
console.error('[Service Worker] Scan error:', error); }
event.source.postMessage({ status: 'error', message: error.message });
} event.ports[0].postMessage({ status: 'success', message: '' });
} } catch (error) {
}); event.ports[0].postMessage({ status: 'error', message: error.message });
}
// ============================================ } else if (data.type === 'BATCH_WRITING') {
// DATABASE COMMUNICATION const { storeName, objects } = data.payload;
// ============================================ const db = await openDatabase();
const tx = db.transaction(storeName, 'readwrite');
async function requestFromMainThread(client, action, payload) { const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const messageId = `sw_${Date.now()}_${Math.random()}`; for (const { key, object } of objects) {
if (key) {
const messageHandler = (event) => { await store.put(object, key);
if (event.data.id === messageId) { } else {
self.removeEventListener('message', messageHandler); await store.put(object);
if (event.data.type === 'DB_RESPONSE') { }
resolve(event.data.result); }
} else if (event.data.type === 'DB_ERROR') {
reject(new Error(event.data.error)); await tx.done;
} }
} });
};
async function scanMissingData(processesToScan) {
self.addEventListener('message', messageHandler); console.log('Scanning for missing data...');
const myProcesses = await getProcesses(processesToScan);
client.postMessage({
type: 'DB_REQUEST', let toDownload = new Set();
id: messageId, // Iterate on each process
action, if (myProcesses && myProcesses.length != 0) {
payload for (const process of myProcesses) {
}); // Iterate on states
const firstState = process.states[0];
setTimeout(() => { const processId = firstState.commited_in;
self.removeEventListener('message', messageHandler); for (const state of process.states) {
reject(new Error('Database request timeout')); if (state.state_id === EMPTY32BYTES) continue;
}, 10000); // iterate on pcd_commitment
}); for (const [field, hash] of Object.entries(state.pcd_commitment)) {
} // Skip public fields
if (state.public_data[field] !== undefined || field === 'roles') continue;
// ============================================ // Check if we have the data in db
// SCAN LOGIC const existingData = await getBlob(hash);
// ============================================ if (!existingData) {
toDownload.add(hash);
async function scanMissingData(processesToScan, client) { // We also add an entry in diff, in case it doesn't already exist
console.log('[Service Worker] Scanning for missing data...'); await addDiff(processId, state.state_id, hash, state.roles, field);
} else {
const myProcesses = await requestFromMainThread(client, 'GET_MULTIPLE_OBJECTS', { // We remove it if we have it in the set
storeName: 'processes', if (toDownload.delete(hash)) {
keys: processesToScan console.log(`Removing ${hash} from the set`);
}); }
}
let toDownload = new Set(); }
let diffsToCreate = []; }
}
if (myProcesses && myProcesses.length != 0) { }
for (const process of myProcesses) {
const firstState = process.states[0]; console.log(toDownload);
const processId = firstState.commited_in; return Array.from(toDownload);
for (const state of process.states) { }
if (state.state_id === EMPTY32BYTES) continue;
async function openDatabase() {
for (const [field, hash] of Object.entries(state.pcd_commitment)) { return new Promise((resolve, reject) => {
if (state.public_data[field] !== undefined || field === 'roles') continue; const request = indexedDB.open('4nk', 1);
request.onerror = (event) => {
const existingData = await requestFromMainThread(client, 'GET_OBJECT', { reject(request.error);
storeName: 'data', };
key: hash request.onsuccess = (event) => {
}); resolve(request.result);
};
if (!existingData) { request.onupgradeneeded = (event) => {
toDownload.add(hash); const db = event.target.result;
if (!db.objectStoreNames.contains('wallet')) {
const existingDiff = await requestFromMainThread(client, 'GET_OBJECT', { db.createObjectStore('wallet', { keyPath: 'pre_id' });
storeName: 'diffs', }
key: hash };
}); });
}
if (!existingDiff) {
diffsToCreate.push({ // Function to get all processes because it is asynchronous
process_id: processId, async function getAllProcesses() {
state_id: state.state_id, const db = await openDatabase();
value_commitment: hash, return new Promise((resolve, reject) => {
roles: state.roles, if (!db) {
field: field, reject(new Error('Database is not available'));
description: null, return;
previous_value: null, }
new_value: null, const tx = db.transaction('processes', 'readonly');
notify_user: false, const store = tx.objectStore('processes');
need_validation: false, const request = store.getAll();
validation_status: 'None'
}); request.onsuccess = () => {
} resolve(request.result);
} else { };
if (toDownload.delete(hash)) {
console.log(`[Service Worker] Removing ${hash} from the set`); request.onerror = () => {
} reject(request.error);
} };
} });
} };
}
} async function getProcesses(processIds) {
if (!processIds || processIds.length === 0) {
console.log('[Service Worker] Scan complete:', { toDownload: toDownload.size, diffsToCreate: diffsToCreate.length }); return [];
return { }
toDownload: Array.from(toDownload),
diffsToCreate: diffsToCreate const db = await openDatabase();
}; if (!db) {
} throw new Error('Database is not available');
}
const tx = db.transaction('processes', 'readonly');
const store = tx.objectStore('processes');
const requests = Array.from(processIds).map((processId) => {
return new Promise((resolve) => {
const request = store.get(processId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => {
console.error(`Error fetching process ${processId}:`, request.error);
resolve(undefined);
};
});
});
const results = await Promise.all(requests);
return results.filter(result => result !== undefined);
}
async function getAllDiffsNeedValidation() {
const db = await openDatabase();
const allProcesses = await getAllProcesses();
const tx = db.transaction('diffs', 'readonly');
const store = tx.objectStore('diffs');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = (event) => {
const allItems = event.target.result;
const itemsWithFlag = allItems.filter((item) => item.need_validation);
const processMap = {};
for (const diff of itemsWithFlag) {
const currentProcess = allProcesses.find((item) => {
return item.states.some((state) => state.merkle_root === diff.new_state_merkle_root);
});
if (currentProcess) {
const processKey = currentProcess.merkle_root;
if (!processMap[processKey]) {
processMap[processKey] = {
process: currentProcess.states,
processId: currentProcess.key,
diffs: [],
};
}
processMap[processKey].diffs.push(diff);
}
}
const results = Object.values(processMap).map((entry) => {
const diffs = []
for(const state of entry.process) {
const filteredDiff = entry.diffs.filter(diff => diff.new_state_merkle_root === state.merkle_root);
if(filteredDiff && filteredDiff.length) {
diffs.push(filteredDiff)
}
}
return {
process: entry.process,
processId: entry.processId,
diffs: diffs,
};
});
resolve(results);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
async function getBlob(hash) {
const db = await openDatabase();
const storeName = 'data';
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const result = await new Promise((resolve, reject) => {
const getRequest = store.get(hash);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
return result;
}
async function addDiff(processId, stateId, hash, roles, field) {
const db = await openDatabase();
const storeName = 'diffs';
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
// Check if the diff already exists
const existingDiff = await new Promise((resolve, reject) => {
const getRequest = store.get(hash);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
if (!existingDiff) {
const newDiff = {
process_id: processId,
state_id: stateId,
value_commitment: hash,
roles: roles,
field: field,
description: null,
previous_value: null,
new_value: null,
notify_user: false,
need_validation: false,
validation_status: 'None'
};
const insertResult = await new Promise((resolve, reject) => {
const putRequest = store.put(newDiff);
putRequest.onsuccess = () => resolve(putRequest.result);
putRequest.onerror = () => reject(putRequest.error);
});
return insertResult;
}
return existingDiff;
}

View File

@ -9,11 +9,12 @@ async function bootstrap() {
console.log("🚀 Démarrage de l'application 4NK..."); console.log("🚀 Démarrage de l'application 4NK...");
try { try {
// 1. Initialisation des Services (WASM, Sockets, Database...) // 1. Initialisation de la Base de données
const services = await Services.getInstance(); const db = await Database.getInstance();
db.registerServiceWorker('/database.worker.js');
// 2. Initialisation de la base de données (Web Worker + Service Worker) // 2. Initialisation des Services (WASM, Sockets...)
await Database.getInstance(); const services = await Services.getInstance();
// Injection du Header dans le slot prévu dans index.html // Injection du Header dans le slot prévu dans index.html
const headerSlot = document.getElementById('header-slot'); const headerSlot = document.getElementById('header-slot');

View File

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

View File

@ -24,7 +24,6 @@ export default class Services {
private notifications: any[] | null = null; private notifications: any[] | null = null;
private subscriptions: { element: Element; event: string; eventHandler: string }[] = []; private subscriptions: { element: Element; event: string; eventHandler: string }[] = [];
private database: any; private database: any;
private db!: Database; // Database singleton
private relayAddresses: { [wsurl: string]: string } = {}; private relayAddresses: { [wsurl: string]: string } = {};
private membersList: Record<string, Member> = {}; private membersList: Record<string, Member> = {};
private currentBlockHeight: number = -1; private currentBlockHeight: number = -1;
@ -59,7 +58,6 @@ export default class Services {
this.notifications = this.getNotifications(); this.notifications = this.getNotifications();
this.sdkClient = await import('../../pkg/sdk_client'); this.sdkClient = await import('../../pkg/sdk_client');
this.sdkClient.setup(); this.sdkClient.setup();
this.db = await Database.getInstance(); // Initialiser l'instance DB
for (const wsurl of Object.values(BOOTSTRAPURL)) { for (const wsurl of Object.values(BOOTSTRAPURL)) {
this.updateRelay(wsurl, ''); this.updateRelay(wsurl, '');
} }
@ -194,15 +192,26 @@ export default class Services {
} }
public async getSecretForAddress(address: string): Promise<string | null> { public async getSecretForAddress(address: string): Promise<string | null> {
return await this.db.getSharedSecret(address); const db = await Database.getInstance();
return await db.getObject('shared_secrets', address);
} }
public async getAllSecrets(): Promise<SecretsStore> { public async getAllSecrets(): Promise<SecretsStore> {
return await this.db.getAllSecrets(); const db = await Database.getInstance();
const sharedSecrets = await db.dumpStore('shared_secrets');
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets'); // keys are numeric values
const secretsStore = {
shared_secrets: sharedSecrets,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
return secretsStore;
} }
public async getAllDiffs(): Promise<Record<string, UserDiff>> { public async getAllDiffs(): Promise<Record<string, UserDiff>> {
return await this.db.getAllDiffs(); const db = await Database.getInstance();
return await db.dumpStore('diffs');
} }
/** /**
@ -225,7 +234,10 @@ export default class Services {
} }
public async getDiffByValue(value: string): Promise<UserDiff | null> { public async getDiffByValue(value: string): Promise<UserDiff | null> {
return await this.db.getDiff(value); const db = await Database.getInstance();
const store = 'diffs';
const res = await db.getObject(store, value);
return res;
} }
private async getTokensFromFaucet(): Promise<void> { private async getTokensFromFaucet(): Promise<void> {
@ -829,7 +841,12 @@ export default class Services {
this.sdkClient.reset_device(); this.sdkClient.reset_device();
// Clear all stores // Clear all stores
await this.db.clearMultipleStores(['wallet', 'shared_secrets', 'unconfirmed_secrets', 'processes', 'diffs']); const db = await Database.getInstance();
await db.clearStore('wallet');
await db.clearStore('shared_secrets');
await db.clearStore('unconfirmed_secrets');
await db.clearStore('processes');
await db.clearStore('diffs');
console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.'); console.warn('[Services:resetDevice] ✅ Réinitialisation terminée.');
} }
@ -1033,19 +1050,39 @@ export default class Services {
private async handleSecrets(secrets: any) { private async handleSecrets(secrets: any) {
const { unconfirmed_secrets, shared_secrets } = secrets; const { unconfirmed_secrets, shared_secrets } = secrets;
const db = await Database.getInstance();
const unconfirmedList = unconfirmed_secrets && unconfirmed_secrets.length > 0 ? unconfirmed_secrets : []; // Sauvegarder les secrets non confirmés
const sharedList = shared_secrets && Object.keys(shared_secrets).length > 0 if (unconfirmed_secrets && unconfirmed_secrets.length > 0) {
? Object.entries(shared_secrets).map(([key, value]) => ({ key, value })) console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${unconfirmed_secrets.length} secret(s) non confirmé(s)`);
: []; for (const secret of unconfirmed_secrets) {
try {
await db.addObject({
storeName: 'unconfirmed_secrets',
object: secret,
key: null,
});
} catch (e) {
console.error("[Services:handleSecrets] 💥 Échec de sauvegarde d'un secret non confirmé:", e);
}
}
}
if (unconfirmedList.length > 0 || sharedList.length > 0) { // Sauvegarder les secrets partagés (confirmés)
console.log(`[Services:handleSecrets] 💾 Sauvegarde batch: ${unconfirmedList.length} secret(s) non confirmé(s) + ${sharedList.length} secret(s) partagé(s)`); if (shared_secrets && Object.keys(shared_secrets).length > 0) {
try { const entries = Object.entries(shared_secrets).map(([key, value]) => ({ key, value }));
await this.db.saveSecretsBatch(unconfirmedList, sharedList); console.log(`[Services:handleSecrets] 💾 Sauvegarde de ${entries.length} secret(s) partagé(s)`);
console.log('[Services:handleSecrets] ✅ Secrets sauvegardés en batch.'); for (const entry of entries) {
} catch (e) { try {
console.error('[Services:handleSecrets] 💥 Échec de sauvegarde batch des secrets:', e); await db.addObject({
storeName: 'shared_secrets',
object: entry.value,
key: entry.key,
});
console.log(`[Services:handleSecrets] ✅ Secret partagé pour ${entry.key} sauvegardé.`);
} catch (e) {
console.error(`[Services:handleSecrets] 💥 Échec de l'ajout du secret partagé pour ${entry.key}:`, e);
}
} }
} }
} }
@ -1351,22 +1388,49 @@ export default class Services {
} }
async saveDeviceInDatabase(device: Device): Promise<void> { async saveDeviceInDatabase(device: Device): Promise<void> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try { try {
console.log("[Services:saveDeviceInDatabase] 💾 Sauvegarde de l'appareil en BDD...", { console.log("[Services:saveDeviceInDatabase] 💾 Sauvegarde de l'appareil en BDD...", {
pairing_process_commitment: device.pairing_process_commitment, pairing_process_commitment: device.pairing_process_commitment,
paired_member: device.paired_member, paired_member: device.paired_member,
}); });
await this.db.saveDevice(device); const prevDevice = await this.getDeviceFromDatabase();
if (prevDevice) {
// console.debug('[Services:saveDeviceInDatabase] Appareil précédent trouvé, suppression...');
await db.deleteObject(walletStore, '1');
}
await db.addObject({
storeName: walletStore,
object: { pre_id: '1', device },
key: null,
});
console.log('[Services:saveDeviceInDatabase] ✅ Appareil sauvegardé avec succès'); console.log('[Services:saveDeviceInDatabase] ✅ Appareil sauvegardé avec succès');
// // Verify save
// const savedDevice = await this.getDeviceFromDatabase();
// console.log('[Services:saveDeviceInDatabase] 🔎 Vérification:', {
// pairing_process_commitment: savedDevice?.pairing_process_commitment,
// paired_member: savedDevice?.paired_member,
// });
} catch (e) { } catch (e) {
console.error('[Services:saveDeviceInDatabase] 💥 Erreur lors de la sauvegarde:', e); console.error('[Services:saveDeviceInDatabase] 💥 Erreur lors de la sauvegarde:', e);
} }
} }
async getDeviceFromDatabase(): Promise<Device | null> { async getDeviceFromDatabase(): Promise<Device | null> {
const db = await Database.getInstance();
const walletStore = 'wallet';
try { try {
return await this.db.getDevice(); const dbRes = await db.getObject(walletStore, '1');
if (dbRes) {
return dbRes['device'];
} else {
return null;
}
} catch (e) { } catch (e) {
throw new Error(`[Services:getDeviceFromDatabase] 💥 Échec: ${e}`); throw new Error(`[Services:getDeviceFromDatabase] 💥 Échec: ${e}`);
} }
@ -1517,22 +1581,44 @@ export default class Services {
} }
} }
private async removeProcess(processId: string): Promise<void> {
const db = await Database.getInstance();
const storeName = 'processes';
try {
console.log(`[Services:removeProcess] 🗑️ Suppression du processus ${processId}`);
await db.deleteObject(storeName, processId);
} catch (e) {
console.error(e);
}
}
public async batchSaveProcessesToDb(processes: Record<string, Process>) { public async batchSaveProcessesToDb(processes: Record<string, Process>) {
if (Object.keys(processes).length === 0) { if (Object.keys(processes).length === 0) {
return; return;
} }
console.log(`[Services:batchSaveProcessesToDb] 💾 Sauvegarde de ${Object.keys(processes).length} processus en BDD...`); console.log(`[Services:batchSaveProcessesToDb] 💾 Sauvegarde de ${Object.keys(processes).length} processus en BDD...`);
const db = await Database.getInstance();
const storeName = 'processes';
try { try {
await this.db.saveProcessesBatch(processes); await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
this.processesCache = { ...this.processesCache, ...processes }; this.processesCache = { ...this.processesCache, ...processes };
} catch (e) { } catch (e) {
console.error('[Services:batchSaveProcessesToDb] 💥 Échec:', e); console.error('[Services:batchSaveProcessesToDb] 💥 Échec:', e);
throw e;
} }
} }
public async saveProcessToDb(processId: string, process: Process) { public async saveProcessToDb(processId: string, process: Process) {
const db = await Database.getInstance();
const storeName = 'processes';
try { try {
await this.db.saveProcess(processId, process); await db.addObject({
storeName,
object: process,
key: processId,
});
// Update the process in the cache // Update the process in the cache
this.processesCache[processId] = process; this.processesCache[processId] = process;
} catch (e) { } catch (e) {
@ -1541,81 +1627,27 @@ export default class Services {
} }
public async saveBlobToDb(hash: string, data: Blob) { public async saveBlobToDb(hash: string, data: Blob) {
const db = await Database.getInstance();
try { try {
await this.db.saveBlob(hash, data); await db.addObject({
storeName: 'data',
object: data,
key: hash,
});
} catch (e) { } catch (e) {
console.error(`[Services:saveBlobToDb] 💥 Échec de la sauvegarde du blob ${hash}: ${e}`); console.error(`[Services:saveBlobToDb] 💥 Échec de la sauvegarde du blob ${hash}: ${e}`);
} }
} }
public async getBlobFromDb(hash: string): Promise<Blob | null> { public async getBlobFromDb(hash: string): Promise<Blob | null> {
const db = await Database.getInstance();
try { try {
return await this.db.getBlob(hash); return await db.getObject('data', hash);
} catch (e) { } catch (e) {
return null; return null;
} }
} }
public async getProcess(processId: string): Promise<Process | null> {
// 1. Essayer le cache en mémoire
if (this.processesCache[processId]) {
return this.processesCache[processId];
}
// 2. Si non trouvé, essayer la BDD
try {
const process = await this.db.getProcess(processId);
if (process) {
this.processesCache[processId] = process; // Mettre en cache
}
return process;
} catch (e) {
console.error(`[Services:getProcess] 💥 Échec de la récupération du processus ${processId}: ${e}`);
return null;
}
}
public async getProcesses(): Promise<Record<string, Process>> {
// 1. Essayer le cache en mémoire
if (Object.keys(this.processesCache).length > 0) {
return this.processesCache;
}
// 2. Si non trouvé, charger depuis la BDD
try {
console.log('[Services:getProcesses] Cache de processus vide. Chargement depuis la BDD...');
this.processesCache = await this.db.getAllProcesses();
console.log(`[Services:getProcesses] ✅ ${Object.keys(this.processesCache).length} processus chargés en cache.`);
return this.processesCache;
} catch (e) {
console.error('[Services:getProcesses] 💥 Échec:', e);
return {};
}
}
public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
console.log(`[Services:restoreProcessesFromBackUp] 💾 Restauration de ${Object.keys(processes).length} processus depuis un backup...`);
try {
await this.db.saveProcessesBatch(processes);
} catch (e) {
throw e;
}
}
// Restore processes cache from persistent storage
public async restoreProcessesFromDB() {
try {
const processes: Record<string, Process> = await this.db.getAllProcesses();
if (processes && Object.keys(processes).length != 0) {
console.log(`[Services:restoreProcessesFromDB] 🔄 Restauration de ${Object.keys(processes).length} processus depuis la BDD vers le cache...`);
this.processesCache = processes;
console.log('[Services:restoreProcessesFromDB] ✅ Processus restaurés.');
}
} catch (e) {
throw e;
}
}
public async saveDataToStorage(storages: string[], hash: string, data: Blob, ttl: number | null) { public async saveDataToStorage(storages: string[], hash: string, data: Blob, ttl: number | null) {
try { try {
await storeData(storages, hash, data, ttl); await storeData(storages, hash, data, ttl);
@ -1631,23 +1663,113 @@ export default class Services {
} }
public async getDiffByValueFromDb(hash: string): Promise<UserDiff | null> { public async getDiffByValueFromDb(hash: string): Promise<UserDiff | null> {
return await this.db.getDiff(hash); const db = await Database.getInstance();
const diff = await db.getObject('diffs', hash);
return diff;
} }
public async saveDiffsToDb(diffs: UserDiff[]) { public async saveDiffsToDb(diffs: UserDiff[]) {
const db = await Database.getInstance();
try { try {
await this.db.saveDiffs(diffs); for (const diff of diffs) {
await db.addObject({
storeName: 'diffs',
object: diff,
key: null,
});
}
} catch (e) { } catch (e) {
throw new Error(`[Services:saveDiffsToDb] 💥 Échec: ${e}`); throw new Error(`[Services:saveDiffsToDb] 💥 Échec: ${e}`);
} }
} }
public async getProcess(processId: string): Promise<Process | null> {
// 1. Essayer le cache en mémoire
if (this.processesCache[processId]) {
return this.processesCache[processId];
}
// 2. Si non trouvé, essayer la BDD
try {
const db = await Database.getInstance();
const process = await db.getObject('processes', processId);
if (process) {
this.processesCache[processId] = process; // Mettre en cache
}
return process;
} catch (e) {
console.error(`[Services:getProcess] 💥 Échec de récupération du processus ${processId}:`, e);
return null;
}
}
public async getProcesses(): Promise<Record<string, Process>> {
// 1. Essayer le cache en mémoire
if (Object.keys(this.processesCache).length > 0) {
return this.processesCache;
}
// 2. Si non trouvé, charger depuis la BDD
try {
console.log('[Services:getProcesses] Cache de processus vide. Chargement depuis la BDD...');
const db = await Database.getInstance();
this.processesCache = await db.dumpStore('processes');
console.log(`[Services:getProcesses] ✅ ${Object.keys(this.processesCache).length} processus chargés en cache.`);
return this.processesCache;
} catch (e) {
console.error('[Services:getProcesses] 💥 Échec du chargement des processus:', e);
throw e;
}
}
public async restoreProcessesFromBackUp(processes: Record<string, Process>) {
console.log(`[Services:restoreProcessesFromBackUp] 💾 Restauration de ${Object.keys(processes).length} processus depuis un backup...`);
const db = await Database.getInstance();
const storeName = 'processes';
try {
await db.batchWriting({ storeName, objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })) });
} catch (e) {
throw e;
}
await this.restoreProcessesFromDB();
}
// Restore processes cache from persistent storage
public async restoreProcessesFromDB() {
const db = await Database.getInstance();
try {
const processes: Record<string, Process> = await db.dumpStore('processes');
if (processes && Object.keys(processes).length != 0) {
console.log(`[Services:restoreProcessesFromDB] 🔄 Restauration de ${Object.keys(processes).length} processus depuis la BDD vers le cache...`);
this.processesCache = processes;
} else {
console.log('[Services:restoreProcessesFromDB] Aucun processus à restaurer.');
}
} catch (e) {
throw e;
}
}
public async restoreSecretsFromBackUp(secretsStore: SecretsStore) { public async restoreSecretsFromBackUp(secretsStore: SecretsStore) {
console.log('[Services:restoreSecretsFromBackUp] 💾 Restauration des secrets depuis un backup...'); console.log('[Services:restoreSecretsFromBackUp] 💾 Restauration des secrets depuis un backup...');
const db = await Database.getInstance();
const sharedList = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value })); for (const secret of secretsStore.unconfirmed_secrets) {
await this.db.saveSecretsBatch(secretsStore.unconfirmed_secrets, sharedList); await db.addObject({
console.log('[Services:restoreSecretsFromBackUp] ✅ Secrets restaurés en batch.'); storeName: 'unconfirmed_secrets',
object: secret,
key: null,
});
}
const entries = Object.entries(secretsStore.shared_secrets).map(([key, value]) => ({ key, value }));
for (const entry of entries) {
await db.addObject({
storeName: 'shared_secrets',
object: entry.value,
key: entry.key,
});
}
// Now we can transfer them to memory // Now we can transfer them to memory
await this.restoreSecretsFromDB(); await this.restoreSecretsFromDB();
@ -1655,10 +1777,16 @@ export default class Services {
public async restoreSecretsFromDB() { public async restoreSecretsFromDB() {
console.log('[Services:restoreSecretsFromDB] 🔄 Restauration des secrets depuis la BDD vers la mémoire SDK...'); console.log('[Services:restoreSecretsFromDB] 🔄 Restauration des secrets depuis la BDD vers la mémoire SDK...');
const db = await Database.getInstance();
try { try {
const secretsStore = await this.db.getAllSecrets(); const sharedSecrets: Record<string, string> = await db.dumpStore('shared_secrets');
const unconfirmedSecrets = await db.dumpStore('unconfirmed_secrets');
const secretsStore = {
shared_secrets: sharedSecrets,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore)); this.sdkClient.set_shared_secrets(JSON.stringify(secretsStore));
console.log(`[Services:restoreSecretsFromDB] ✅ ${Object.keys(secretsStore.shared_secrets).length} secrets partagés restaurés.`); console.log(`[Services:restoreSecretsFromDB] ✅ ${Object.keys(sharedSecrets).length} secrets partagés restaurés.`);
} catch (e) { } catch (e) {
throw e; throw e;
} }

View File

@ -1,381 +0,0 @@
/**
* Database Web Worker - Handles all IndexedDB operations in background
*/
import type {
StoreDefinition,
WorkerMessagePayload,
WorkerMessageResponse,
BatchWriteItem
} from './worker.types';
const DB_NAME = '4nk';
const DB_VERSION = 1;
// ============================================
// STORE DEFINITIONS
// ============================================
const STORE_DEFINITIONS: Record<string, StoreDefinition> = {
AnkLabels: {
name: 'labels',
options: { keyPath: 'emoji' },
indices: [],
},
AnkWallet: {
name: 'wallet',
options: { keyPath: 'pre_id' },
indices: [],
},
AnkProcess: {
name: 'processes',
options: {},
indices: [],
},
AnkSharedSecrets: {
name: 'shared_secrets',
options: {},
indices: [],
},
AnkUnconfirmedSecrets: {
name: 'unconfirmed_secrets',
options: { autoIncrement: true },
indices: [],
},
AnkPendingDiffs: {
name: 'diffs',
options: { keyPath: 'value_commitment' },
indices: [
{ 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: {
name: 'data',
options: {},
indices: [],
},
};
let db: IDBDatabase | null = null;
// ============================================
// DATABASE INITIALIZATION
// ============================================
async function openDatabase(): Promise<IDBDatabase> {
if (db) {
return db;
}
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const database = (event.target as IDBOpenDBRequest).result;
Object.values(STORE_DEFINITIONS).forEach(({ name, options, indices }) => {
if (!database.objectStoreNames.contains(name)) {
const store = database.createObjectStore(name, options);
indices.forEach(({ name: indexName, keyPath, options: indexOptions }) => {
store.createIndex(indexName, keyPath, indexOptions);
});
}
});
};
request.onsuccess = () => {
db = request.result;
resolve(db);
};
request.onerror = () => {
reject(request.error);
};
});
}
// ============================================
// WRITE OPERATIONS
// ============================================
async function addObject(storeName: string, object: any, key?: IDBValidKey): Promise<{ success: boolean }> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
let request: IDBRequest;
if (key !== null && key !== undefined) {
request = store.put(object, key);
} else {
request = store.put(object);
}
request.onsuccess = () => resolve({ success: true });
request.onerror = () => reject(request.error);
});
}
async function batchWriting(storeName: string, objects: BatchWriteItem[]): Promise<{ success: boolean }> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
for (const { key, object } of objects) {
if (key !== null && key !== undefined) {
store.put(object, key);
} else {
store.put(object);
}
}
return new Promise((resolve, reject) => {
tx.oncomplete = () => resolve({ success: true });
tx.onerror = () => reject(tx.error);
});
}
// ============================================
// READ OPERATIONS
// ============================================
async function getObject(storeName: string, key: IDBValidKey): Promise<any> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result ?? null);
request.onerror = () => reject(request.error);
});
}
async function dumpStore(storeName: string): Promise<Record<string, any>> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const result: Record<string, any> = {};
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
if (cursor) {
result[cursor.key as string] = cursor.value;
cursor.continue();
} else {
resolve(result);
}
};
request.onerror = () => reject(request.error);
});
}
async function getAllObjects(storeName: string): Promise<any[]> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result || []);
request.onerror = () => reject(request.error);
});
}
async function getMultipleObjects(storeName: string, keys: IDBValidKey[]): Promise<any[]> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const requests = keys.map((key) => {
return new Promise<any>((resolve) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => {
console.error(`Error fetching key ${key}:`, request.error);
resolve(null);
};
});
});
const results = await Promise.all(requests);
return results.filter(result => result !== null);
}
async function getAllObjectsWithFilter(storeName: string, filterFn?: string): Promise<any[]> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => {
const allItems = request.result || [];
if (filterFn) {
const filter = new Function('item', `return ${filterFn}`) as (item: any) => boolean;
resolve(allItems.filter(filter));
} else {
resolve(allItems);
}
};
request.onerror = () => reject(request.error);
});
}
// ============================================
// DELETE OPERATIONS
// ============================================
async function deleteObject(storeName: string, key: IDBValidKey): Promise<{ success: boolean }> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve({ success: true });
request.onerror = () => reject(request.error);
});
}
async function clearStore(storeName: string): Promise<{ success: boolean }> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.clear();
request.onsuccess = () => resolve({ success: true });
request.onerror = () => reject(request.error);
});
}
// ============================================
// INDEX OPERATIONS
// ============================================
async function requestStoreByIndex(storeName: string, indexName: string, requestValue: IDBValidKey): Promise<any[]> {
const database = await openDatabase();
const tx = database.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const index = store.index(indexName);
return new Promise((resolve, reject) => {
const request = index.getAll(requestValue);
request.onsuccess = () => {
const allItems = request.result;
const filtered = allItems.filter((item: any) => item.state_id === requestValue);
resolve(filtered);
};
request.onerror = () => reject(request.error);
});
}
// ============================================
// UTILITY FUNCTIONS
// ============================================
function getStoreList(): Record<string, string> {
const storeList: Record<string, string> = {};
Object.keys(STORE_DEFINITIONS).forEach((key) => {
storeList[key] = STORE_DEFINITIONS[key].name;
});
return storeList;
}
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener('message', async (event: MessageEvent<WorkerMessagePayload>) => {
const { type, payload, id } = event.data;
try {
let result: any;
switch (type) {
case 'INIT':
await openDatabase();
result = { success: true };
break;
case 'ADD_OBJECT':
result = await addObject(payload.storeName, payload.object, payload.key);
break;
case 'BATCH_WRITING':
result = await batchWriting(payload.storeName, payload.objects);
break;
case 'GET_OBJECT':
result = await getObject(payload.storeName, payload.key);
break;
case 'DUMP_STORE':
result = await dumpStore(payload.storeName);
break;
case 'DELETE_OBJECT':
result = await deleteObject(payload.storeName, payload.key);
break;
case 'CLEAR_STORE':
result = await clearStore(payload.storeName);
break;
case 'REQUEST_STORE_BY_INDEX':
result = await requestStoreByIndex(
payload.storeName,
payload.indexName,
payload.request
);
break;
case 'GET_ALL_OBJECTS':
result = await getAllObjects(payload.storeName);
break;
case 'GET_MULTIPLE_OBJECTS':
result = await getMultipleObjects(payload.storeName, payload.keys);
break;
case 'GET_ALL_OBJECTS_WITH_FILTER':
result = await getAllObjectsWithFilter(payload.storeName, payload.filterFn);
break;
case 'GET_STORE_LIST':
result = getStoreList();
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
self.postMessage({
id,
type: 'SUCCESS',
result,
} as WorkerMessageResponse);
} catch (error) {
self.postMessage({
id,
type: 'ERROR',
error: (error as Error).message || String(error),
} as WorkerMessageResponse);
}
});
// ============================================
// INITIALIZATION
// ============================================
openDatabase().catch((error) => {
console.error('[Database Worker] Failed to initialize database:', error);
});

View File

@ -1,33 +0,0 @@
/**
* Shared types for Web Workers
*/
export interface StoreDefinition {
name: string;
options: IDBObjectStoreParameters;
indices: IndexDefinition[];
}
export interface IndexDefinition {
name: string;
keyPath: string | string[];
options: IDBIndexParameters;
}
export interface WorkerMessagePayload {
type: string;
payload?: any;
id: number;
}
export interface WorkerMessageResponse {
id: number;
type: 'SUCCESS' | 'ERROR';
result?: any;
error?: string;
}
export interface BatchWriteItem {
key?: IDBValidKey;
object: any;
}