refactor(data.worker): optimize database access in service worker by implementing direct IndexedDB functions and enhancing message handling

This commit is contained in:
NicolasCantu 2025-11-28 00:13:42 +01:00
parent bbbca27009
commit 8a87fe38c5
2 changed files with 288 additions and 223 deletions

View File

@ -1,82 +1,75 @@
const EMPTY32BYTES = String('').padStart(64, '0');
// public/data.worker.js
const DB_NAME = "4nk";
const DB_VERSION = 1;
const EMPTY32BYTES = String("").padStart(64, "0");
// ============================================
// SERVICE WORKER LIFECYCLE
// ============================================
self.addEventListener('install', (event) => {
self.addEventListener("install", (event) => {
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', (event) => {
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
// ============================================
// MESSAGE HANDLER
// INDEXEDDB DIRECT ACCESS (READ-ONLY)
// ============================================
self.addEventListener('message', async (event) => {
const data = event.data;
console.log('[Service Worker] Message received:', data.type);
if (data.type === 'SCAN') {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length != 0) {
const scanResult = await scanMissingData(myProcessesId, event.source);
if (scanResult.toDownload.length != 0) {
console.log('[Service Worker] Sending TO_DOWNLOAD message');
event.source.postMessage({ type: 'TO_DOWNLOAD', data: scanResult.toDownload });
}
if (scanResult.diffsToCreate.length > 0) {
console.log('[Service Worker] Sending DIFFS_TO_CREATE message');
event.source.postMessage({ type: 'DIFFS_TO_CREATE', data: scanResult.diffsToCreate });
}
} else {
event.source.postMessage({ status: 'error', message: 'Empty lists' });
}
} catch (error) {
console.error('[Service Worker] Scan error:', error);
event.source.postMessage({ status: 'error', message: error.message });
}
}
});
// ============================================
// DATABASE COMMUNICATION
// ============================================
async function requestFromMainThread(client, action, payload) {
/**
* Ouvre une connexion à la BDD directement depuis le Service Worker
*/
function openDB() {
return new Promise((resolve, reject) => {
const messageId = `sw_${Date.now()}_${Math.random()}`;
const messageHandler = (event) => {
if (event.data.id === messageId) {
self.removeEventListener('message', messageHandler);
if (event.data.type === 'DB_RESPONSE') {
resolve(event.data.result);
} else if (event.data.type === 'DB_ERROR') {
reject(new Error(event.data.error));
}
}
};
self.addEventListener('message', messageHandler);
client.postMessage({
type: 'DB_REQUEST',
id: messageId,
action,
payload
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
setTimeout(() => {
self.removeEventListener('message', messageHandler);
reject(new Error('Database request timeout'));
}, 10000);
/**
* Récupère un objet spécifique (équivalent à GET_OBJECT)
*/
function getObject(db, storeName, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
/**
* Récupère plusieurs objets d'un coup (équivalent à GET_MULTIPLE_OBJECTS)
* Optimisé pour utiliser une seule transaction.
*/
function getMultipleObjects(db, storeName, keys) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const results = [];
let completed = 0;
if (keys.length === 0) resolve([]);
keys.forEach((key) => {
const request = store.get(key);
request.onsuccess = () => {
if (request.result) results.push(request.result);
completed++;
if (completed === keys.length) resolve(results);
};
request.onerror = () => {
console.warn(`[SW] Erreur lecture clé ${key}`);
completed++;
if (completed === keys.length) resolve(results);
};
});
});
}
@ -84,39 +77,56 @@ async function requestFromMainThread(client, action, payload) {
// SCAN LOGIC
// ============================================
async function scanMissingData(processesToScan, client) {
console.log('[Service Worker] Scanning for missing data...');
async function scanMissingData(processesToScan) {
console.log("[Service Worker] 🚀 Scanning with DIRECT DB ACCESS...");
const myProcesses = await requestFromMainThread(client, 'GET_MULTIPLE_OBJECTS', {
storeName: 'processes',
keys: processesToScan
});
let db;
try {
db = await openDB();
} catch (e) {
console.error("[SW] Impossible d'ouvrir la BDD:", e);
return { toDownload: [], diffsToCreate: [] };
}
// 1. Récupération directe des processus
const myProcesses = await getMultipleObjects(
db,
"processes",
processesToScan
);
let toDownload = new Set();
let diffsToCreate = [];
if (myProcesses && myProcesses.length != 0) {
if (myProcesses && myProcesses.length !== 0) {
for (const process of myProcesses) {
if (!process || !process.states) continue;
const firstState = process.states[0];
// Sécurisation : on vérifie que firstState existe
if (!firstState) continue;
const processId = firstState.commited_in;
for (const state of process.states) {
if (state.state_id === EMPTY32BYTES) continue;
for (const [field, hash] of Object.entries(state.pcd_commitment)) {
if (state.public_data[field] !== undefined || field === 'roles') continue;
// On ignore les données publiques ou les rôles
if (
(state.public_data && state.public_data[field] !== undefined) ||
field === "roles"
)
continue;
const existingData = await requestFromMainThread(client, 'GET_OBJECT', {
storeName: 'data',
key: hash
});
// 2. Vérification directe dans 'data'
const existingData = await getObject(db, "data", hash);
if (!existingData) {
toDownload.add(hash);
const existingDiff = await requestFromMainThread(client, 'GET_OBJECT', {
storeName: 'diffs',
key: hash
});
// 3. Vérification directe dans 'diffs'
const existingDiff = await getObject(db, "diffs", hash);
if (!existingDiff) {
diffsToCreate.push({
@ -130,12 +140,13 @@ async function scanMissingData(processesToScan, client) {
new_value: null,
notify_user: false,
need_validation: false,
validation_status: 'None'
validation_status: "None",
});
}
} else {
if (toDownload.delete(hash)) {
console.log(`[Service Worker] Removing ${hash} from the set`);
// Si on a trouvé la donnée, on est sûr de ne pas avoir besoin de la télécharger
if (toDownload.has(hash)) {
toDownload.delete(hash);
}
}
}
@ -143,10 +154,50 @@ async function scanMissingData(processesToScan, client) {
}
}
console.log('[Service Worker] Scan complete:', { toDownload: toDownload.size, diffsToCreate: diffsToCreate.length });
// On ferme la connexion BDD pour libérer les ressources
db.close();
console.log("[Service Worker] Scan complete:", {
toDownload: toDownload.size,
diffsToCreate: diffsToCreate.length,
});
return {
toDownload: Array.from(toDownload),
diffsToCreate: diffsToCreate
diffsToCreate: diffsToCreate,
};
}
// ============================================
// MESSAGE HANDLER
// ============================================
self.addEventListener("message", async (event) => {
const data = event.data;
if (data.type === "SCAN") {
try {
const myProcessesId = data.payload;
if (myProcessesId && myProcessesId.length !== 0) {
// Appel direct de la nouvelle fonction optimisée
const scanResult = await scanMissingData(myProcessesId);
if (scanResult.toDownload.length !== 0) {
event.source.postMessage({
type: "TO_DOWNLOAD",
data: scanResult.toDownload,
});
}
if (scanResult.diffsToCreate.length > 0) {
event.source.postMessage({
type: "DIFFS_TO_CREATE",
data: scanResult.diffsToCreate,
});
}
}
} catch (error) {
console.error("[Service Worker] Scan error:", error);
// On évite de spammer l'UI avec des erreurs internes du worker
}
}
});

View File

@ -1,4 +1,4 @@
import Services from './service';
import Services from "./service";
/**
* Database service managing IndexedDB operations via Web Worker and Service Worker
@ -13,7 +13,10 @@ export class Database {
private serviceWorkerCheckIntervalId: number | null = null;
private indexedDBWorker: Worker | null = null;
private messageIdCounter: number = 0;
private pendingMessages: Map<number, { resolve: (value: any) => void; reject: (error: any) => void }> = new Map();
private pendingMessages: Map<
number,
{ resolve: (value: any) => void; reject: (error: any) => void }
> = new Map();
// ============================================
// INITIALIZATION & SINGLETON
@ -37,7 +40,10 @@ export class Database {
// ============================================
private initIndexedDBWorker(): void {
this.indexedDBWorker = new Worker(new URL('../workers/database.worker.ts', import.meta.url), { type: 'module' });
this.indexedDBWorker = new Worker(
new URL("../workers/database.worker.ts", import.meta.url),
{ type: "module" }
);
this.indexedDBWorker.onmessage = (event) => {
const { id, type, result, error } = event.data;
@ -46,27 +52,27 @@ export class Database {
if (pending) {
this.pendingMessages.delete(id);
if (type === 'SUCCESS') {
if (type === "SUCCESS") {
pending.resolve(result);
} else if (type === 'ERROR') {
} else if (type === "ERROR") {
pending.reject(new Error(error));
}
}
};
this.indexedDBWorker.onerror = (error) => {
console.error('[Database] IndexedDB Worker error:', error);
console.error("[Database] IndexedDB Worker error:", error);
};
}
private async waitForWorkerReady(): Promise<void> {
return this.sendMessageToWorker('INIT', {});
return this.sendMessageToWorker("INIT", {});
}
private sendMessageToWorker<T = any>(type: string, payload: any): Promise<T> {
return new Promise((resolve, reject) => {
if (!this.indexedDBWorker) {
reject(new Error('IndexedDB Worker not initialized'));
reject(new Error("IndexedDB Worker not initialized"));
return;
}
@ -90,74 +96,93 @@ export class Database {
// ============================================
private initServiceWorker(): void {
this.registerServiceWorker('/data.worker.js');
this.registerServiceWorker("/data.worker.js");
}
private async registerServiceWorker(path: string): Promise<void> {
if (!('serviceWorker' in navigator)) return;
console.log('[Database] Initializing Service Worker:', path);
if (!("serviceWorker" in navigator)) return;
console.log("[Database] Initializing Service Worker:", path);
try {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
const scriptURL = registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL;
const scriptURL =
registration.active?.scriptURL ||
registration.installing?.scriptURL ||
registration.waiting?.scriptURL;
const scope = registration.scope;
if (scope.includes('/src/service-workers/') || (scriptURL && scriptURL.includes('/src/service-workers/'))) {
if (
scope.includes("/src/service-workers/") ||
(scriptURL && scriptURL.includes("/src/service-workers/"))
) {
console.warn(`[Database] Removing old Service Worker (${scope})`);
await registration.unregister();
}
}
const existingValidWorker = registrations.find((r) => {
const url = r.active?.scriptURL || r.installing?.scriptURL || r.waiting?.scriptURL;
return url && url.endsWith(path.replace(/^\//,''));
const url =
r.active?.scriptURL ||
r.installing?.scriptURL ||
r.waiting?.scriptURL;
return url && url.endsWith(path.replace(/^\//, ""));
});
if (!existingValidWorker) {
console.log('[Database] Registering new Service Worker');
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module', scope: '/' });
console.log("[Database] Registering new Service Worker");
this.serviceWorkerRegistration = await navigator.serviceWorker.register(
path,
{ type: "module", scope: "/" }
);
} else {
console.log('[Database] Service Worker already active');
console.log("[Database] Service Worker already active");
this.serviceWorkerRegistration = existingValidWorker;
await this.serviceWorkerRegistration.update();
}
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.type === 'DB_REQUEST') {
await this.handleDatabaseRequest(event.data);
return;
}
navigator.serviceWorker.addEventListener("message", async (event) => {
// ✅ SIMPLIFICATION : Plus besoin de gérer les "DB_REQUEST"
await this.handleServiceWorkerMessage(event.data);
});
if (this.serviceWorkerCheckIntervalId) clearInterval(this.serviceWorkerCheckIntervalId);
if (this.serviceWorkerCheckIntervalId)
clearInterval(this.serviceWorkerCheckIntervalId);
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
const activeWorker = this.serviceWorkerRegistration?.active || (await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration!));
const activeWorker =
this.serviceWorkerRegistration?.active ||
(await this.waitForServiceWorkerActivation(
this.serviceWorkerRegistration!
));
const service = await Services.getInstance();
const payload = await service.getMyProcesses();
if (payload && payload.length != 0) {
activeWorker?.postMessage({ type: 'SCAN', payload });
activeWorker?.postMessage({ type: "SCAN", payload });
}
}, 5000);
} catch (error) {
console.error('[Database] Service Worker error:', error);
console.error("[Database] Service Worker error:", error);
}
}
private async waitForServiceWorkerActivation(registration: ServiceWorkerRegistration): Promise<ServiceWorker | null> {
private async waitForServiceWorkerActivation(
registration: ServiceWorkerRegistration
): Promise<ServiceWorker | null> {
return new Promise((resolve) => {
if (registration.active) {
resolve(registration.active);
} else {
const listener = () => {
if (registration.active) {
navigator.serviceWorker.removeEventListener('controllerchange', listener);
navigator.serviceWorker.removeEventListener(
"controllerchange",
listener
);
resolve(registration.active);
}
};
navigator.serviceWorker.addEventListener('controllerchange', listener);
navigator.serviceWorker.addEventListener("controllerchange", listener);
}
});
}
@ -168,10 +193,12 @@ export class Database {
await this.serviceWorkerRegistration.update();
if (this.serviceWorkerRegistration.waiting) {
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
this.serviceWorkerRegistration.waiting.postMessage({
type: "SKIP_WAITING",
});
}
} catch (error) {
console.error('Error checking for service worker updates:', error);
console.error("Error checking for service worker updates:", error);
}
}
}
@ -179,73 +206,34 @@ export class Database {
// ============================================
// SERVICE WORKER MESSAGE HANDLERS
// ============================================
private async handleDatabaseRequest(request: any): Promise<void> {
const { id, action, payload } = request;
try {
let result;
switch (action) {
case 'GET_OBJECT':
result = await this.getObject(payload.storeName, payload.key);
break;
case 'GET_MULTIPLE_OBJECTS':
result = await this.sendMessageToWorker('GET_MULTIPLE_OBJECTS', payload);
break;
case 'GET_ALL_OBJECTS':
result = await this.sendMessageToWorker('GET_ALL_OBJECTS', payload);
break;
case 'GET_ALL_OBJECTS_WITH_FILTER':
result = await this.sendMessageToWorker('GET_ALL_OBJECTS_WITH_FILTER', payload);
break;
default:
throw new Error(`Unknown database action: ${action}`);
}
if (this.serviceWorkerRegistration?.active) {
this.serviceWorkerRegistration.active.postMessage({
type: 'DB_RESPONSE',
id,
result
});
}
} catch (error: any) {
console.error('[Database] Error handling database request:', error);
if (this.serviceWorkerRegistration?.active) {
this.serviceWorkerRegistration.active.postMessage({
type: 'DB_ERROR',
id,
error: error.message || String(error)
});
}
}
}
// ✅ NETTOYAGE : handleDatabaseRequest() a été supprimé
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case 'TO_DOWNLOAD':
case "TO_DOWNLOAD":
await this.handleDownloadList(message.data);
break;
case 'DIFFS_TO_CREATE':
case "DIFFS_TO_CREATE":
await this.handleDiffsToCreate(message.data);
break;
default:
console.warn('Unknown message type received from service worker:', message);
console.warn(
"Unknown message type received from service worker:",
message
);
}
}
private async handleDiffsToCreate(diffs: any[]): Promise<void> {
console.log(`[Database] Creating ${diffs.length} diffs from Service Worker scan`);
console.log(
`[Database] Creating ${diffs.length} diffs from Service Worker scan`
);
try {
await this.saveDiffs(diffs);
console.log('[Database] Diffs created successfully');
console.log("[Database] Diffs created successfully");
} catch (error) {
console.error('[Database] Error creating diffs:', error);
console.error("[Database] Error creating diffs:", error);
}
}
@ -264,15 +252,17 @@ export class Database {
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
const blob = new Blob([valueBytes], { type: 'application/octet-stream' });
const blob = new Blob([valueBytes], {
type: "application/octet-stream",
});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(
new CustomEvent('newDataReceived', {
new CustomEvent("newDataReceived", {
detail: { processId, stateId, hash },
}),
})
);
} else {
console.log('Request data from managers of the process');
console.log("Request data from managers of the process");
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
@ -289,35 +279,50 @@ export class Database {
// ============================================
public async getStoreList(): Promise<{ [key: string]: string }> {
return this.sendMessageToWorker('GET_STORE_LIST', {});
return this.sendMessageToWorker("GET_STORE_LIST", {});
}
public async addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
await this.sendMessageToWorker('ADD_OBJECT', payload);
public async addObject(payload: {
storeName: string;
object: any;
key: any;
}): Promise<void> {
await this.sendMessageToWorker("ADD_OBJECT", payload);
}
public async batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
await this.sendMessageToWorker('BATCH_WRITING', payload);
public async batchWriting(payload: {
storeName: string;
objects: { key: any; object: any }[];
}): Promise<void> {
await this.sendMessageToWorker("BATCH_WRITING", payload);
}
public async getObject(storeName: string, key: string): Promise<any | null> {
return this.sendMessageToWorker('GET_OBJECT', { storeName, key });
return this.sendMessageToWorker("GET_OBJECT", { storeName, key });
}
public async dumpStore(storeName: string): Promise<Record<string, any>> {
return this.sendMessageToWorker('DUMP_STORE', { storeName });
return this.sendMessageToWorker("DUMP_STORE", { storeName });
}
public async deleteObject(storeName: string, key: string): Promise<void> {
await this.sendMessageToWorker('DELETE_OBJECT', { storeName, key });
await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key });
}
public async clearStore(storeName: string): Promise<void> {
await this.sendMessageToWorker('CLEAR_STORE', { storeName });
await this.sendMessageToWorker("CLEAR_STORE", { storeName });
}
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
return this.sendMessageToWorker('REQUEST_STORE_BY_INDEX', { storeName, indexName, request });
public async requestStoreByIndex(
storeName: string,
indexName: string,
request: string
): Promise<any[]> {
return this.sendMessageToWorker("REQUEST_STORE_BY_INDEX", {
storeName,
indexName,
request,
});
}
public async clearMultipleStores(storeNames: string[]): Promise<void> {
@ -332,24 +337,22 @@ export class Database {
public async saveDevice(device: any): Promise<void> {
try {
const existing = await this.getObject('wallet', '1');
const existing = await this.getObject("wallet", "1");
if (existing) {
await this.deleteObject('wallet', '1');
await this.deleteObject("wallet", "1");
}
} catch (e) {}
await this.addObject({
storeName: 'wallet',
object: { pre_id: '1', device },
storeName: "wallet",
object: { pre_id: "1", device },
key: null,
});
}
public async getDevice(): Promise<any | null> {
const result = await this.getObject('wallet', '1');
console.log(result);
return result ? result['device'] : null;
const result = await this.getObject("wallet", "1");
return result ? result["device"] : null;
}
// ============================================
@ -358,27 +361,32 @@ export class Database {
public async saveProcess(processId: string, process: any): Promise<void> {
await this.addObject({
storeName: 'processes',
storeName: "processes",
object: process,
key: processId,
});
}
public async saveProcessesBatch(processes: Record<string, any>): Promise<void> {
public async saveProcessesBatch(
processes: Record<string, any>
): Promise<void> {
if (Object.keys(processes).length === 0) return;
await this.batchWriting({
storeName: 'processes',
objects: Object.entries(processes).map(([key, value]) => ({ key, object: value })),
storeName: "processes",
objects: Object.entries(processes).map(([key, value]) => ({
key,
object: value,
})),
});
}
public async getProcess(processId: string): Promise<any | null> {
return this.getObject('processes', processId);
return this.getObject("processes", processId);
}
public async getAllProcesses(): Promise<Record<string, any>> {
return this.dumpStore('processes');
return this.dumpStore("processes");
}
// ============================================
@ -387,14 +395,14 @@ export class Database {
public async saveBlob(hash: string, data: Blob): Promise<void> {
await this.addObject({
storeName: 'data',
storeName: "data",
object: data,
key: hash,
});
}
public async getBlob(hash: string): Promise<Blob | null> {
return this.getObject('data', hash);
return this.getObject("data", hash);
}
// ============================================
@ -406,7 +414,7 @@ export class Database {
for (const diff of diffs) {
await this.addObject({
storeName: 'diffs',
storeName: "diffs",
object: diff,
key: null,
});
@ -414,11 +422,11 @@ export class Database {
}
public async getDiff(hash: string): Promise<any | null> {
return this.getObject('diffs', hash);
return this.getObject("diffs", hash);
}
public async getAllDiffs(): Promise<Record<string, any>> {
return this.dumpStore('diffs');
return this.dumpStore("diffs");
}
// ============================================
@ -426,14 +434,17 @@ export class Database {
// ============================================
public async getSharedSecret(address: string): Promise<string | null> {
return this.getObject('shared_secrets', address);
return this.getObject("shared_secrets", address);
}
public async saveSecretsBatch(unconfirmedSecrets: any[], sharedSecrets: { key: string; value: any }[]): Promise<void> {
public async saveSecretsBatch(
unconfirmedSecrets: any[],
sharedSecrets: { key: string; value: any }[]
): Promise<void> {
if (unconfirmedSecrets && unconfirmedSecrets.length > 0) {
for (const secret of unconfirmedSecrets) {
await this.addObject({
storeName: 'unconfirmed_secrets',
storeName: "unconfirmed_secrets",
object: secret,
key: null,
});
@ -443,7 +454,7 @@ export class Database {
if (sharedSecrets && sharedSecrets.length > 0) {
for (const { key, value } of sharedSecrets) {
await this.addObject({
storeName: 'shared_secrets',
storeName: "shared_secrets",
object: value,
key: key,
});
@ -451,9 +462,12 @@ export class Database {
}
}
public async getAllSecrets(): Promise<{ shared_secrets: Record<string, any>; unconfirmed_secrets: any[] }> {
const sharedSecrets = await this.dumpStore('shared_secrets');
const unconfirmedSecrets = await this.dumpStore('unconfirmed_secrets');
public async getAllSecrets(): Promise<{
shared_secrets: Record<string, any>;
unconfirmed_secrets: any[];
}> {
const sharedSecrets = await this.dumpStore("shared_secrets");
const unconfirmedSecrets = await this.dumpStore("unconfirmed_secrets");
return {
shared_secrets: sharedSecrets,