ihm_client/src/services/database.service.ts

480 lines
14 KiB
TypeScript
Executable File

import Services from "./service";
/**
* Database service managing IndexedDB operations via Web Worker and Service Worker
*/
export class Database {
// ============================================
// PRIVATE PROPERTIES
// ============================================
private static instance: Database;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
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();
// ============================================
// INITIALIZATION & SINGLETON
// ============================================
private constructor() {
this.initIndexedDBWorker();
this.initServiceWorker();
}
public static async getInstance(): Promise<Database> {
if (!Database.instance) {
Database.instance = new Database();
await Database.instance.waitForWorkerReady();
}
return Database.instance;
}
// ============================================
// INDEXEDDB WEB WORKER
// ============================================
private initIndexedDBWorker(): void {
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;
const pending = this.pendingMessages.get(id);
if (pending) {
this.pendingMessages.delete(id);
if (type === "SUCCESS") {
pending.resolve(result);
} else if (type === "ERROR") {
pending.reject(new Error(error));
}
}
};
this.indexedDBWorker.onerror = (error) => {
console.error("[Database] IndexedDB Worker error:", error);
};
}
private async waitForWorkerReady(): Promise<void> {
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"));
return;
}
const id = this.messageIdCounter++;
this.pendingMessages.set(id, { resolve, reject });
this.indexedDBWorker.postMessage({ type, payload, id });
// Timeout de sécurité (30 secondes)
setTimeout(() => {
if (this.pendingMessages.has(id)) {
this.pendingMessages.delete(id);
reject(new Error(`Worker message timeout for type: ${type}`));
}
}, 30000);
});
}
// ============================================
// SERVICE WORKER
// ============================================
private initServiceWorker(): void {
this.registerServiceWorker("/data.worker.js");
}
private async registerServiceWorker(path: string): Promise<void> {
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 scope = registration.scope;
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(/^\//, ""));
});
if (!existingValidWorker) {
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");
this.serviceWorkerRegistration = existingValidWorker;
await this.serviceWorkerRegistration.update();
}
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);
this.serviceWorkerCheckIntervalId = window.setInterval(async () => {
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 });
}
}, 5000);
} catch (error) {
console.error("[Database] Service Worker error:", error);
}
}
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
);
resolve(registration.active);
}
};
navigator.serviceWorker.addEventListener("controllerchange", listener);
}
});
}
private async checkForUpdates(): Promise<void> {
if (this.serviceWorkerRegistration) {
try {
await this.serviceWorkerRegistration.update();
if (this.serviceWorkerRegistration.waiting) {
this.serviceWorkerRegistration.waiting.postMessage({
type: "SKIP_WAITING",
});
}
} catch (error) {
console.error("Error checking for service worker updates:", error);
}
}
}
// ============================================
// SERVICE WORKER MESSAGE HANDLERS
// ============================================
// ✅ NETTOYAGE : handleDatabaseRequest() a été supprimé
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case "TO_DOWNLOAD":
await this.handleDownloadList(message.data);
break;
case "DIFFS_TO_CREATE":
await this.handleDiffsToCreate(message.data);
break;
default:
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`
);
try {
await this.saveDiffs(diffs);
console.log("[Database] Diffs created successfully");
} catch (error) {
console.error("[Database] Error creating diffs:", error);
}
}
private async handleDownloadList(downloadList: string[]): Promise<void> {
let requestedStateId: string[] = [];
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
if (!diff) {
console.warn(`Missing a diff for hash ${hash}`);
continue;
}
const processId = diff.process_id;
const stateId = diff.state_id;
const roles = diff.roles;
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
const blob = new Blob([valueBytes], {
type: "application/octet-stream",
});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(
new CustomEvent("newDataReceived", {
detail: { processId, stateId, hash },
})
);
} else {
console.log("Request data from managers of the process");
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
}
// ============================================
// GENERIC INDEXEDDB OPERATIONS
// ============================================
public async getStoreList(): Promise<{ [key: string]: string }> {
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 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 });
}
public async dumpStore(storeName: string): Promise<Record<string, any>> {
return this.sendMessageToWorker("DUMP_STORE", { storeName });
}
public async deleteObject(storeName: string, key: string): Promise<void> {
await this.sendMessageToWorker("DELETE_OBJECT", { storeName, key });
}
public async clearStore(storeName: string): Promise<void> {
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 clearMultipleStores(storeNames: string[]): Promise<void> {
for (const storeName of storeNames) {
await this.clearStore(storeName);
}
}
// ============================================
// BUSINESS METHODS - DEVICE
// ============================================
public async saveDevice(device: any): Promise<void> {
try {
const existing = await this.getObject("wallet", "1");
if (existing) {
await this.deleteObject("wallet", "1");
}
} catch (e) {}
await this.addObject({
storeName: "wallet",
object: { pre_id: "1", device },
key: null,
});
}
public async getDevice(): Promise<any | null> {
const result = await this.getObject("wallet", "1");
return result ? result["device"] : null;
}
// ============================================
// BUSINESS METHODS - PROCESS
// ============================================
public async saveProcess(processId: string, process: any): Promise<void> {
await this.addObject({
storeName: "processes",
object: process,
key: processId,
});
}
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,
})),
});
}
public async getProcess(processId: string): Promise<any | null> {
return this.getObject("processes", processId);
}
public async getAllProcesses(): Promise<Record<string, any>> {
return this.dumpStore("processes");
}
// ============================================
// BUSINESS METHODS - BLOBS
// ============================================
public async saveBlob(hash: string, data: Blob): Promise<void> {
await this.addObject({
storeName: "data",
object: data,
key: hash,
});
}
public async getBlob(hash: string): Promise<Blob | null> {
return this.getObject("data", hash);
}
// ============================================
// BUSINESS METHODS - DIFFS
// ============================================
public async saveDiffs(diffs: any[]): Promise<void> {
if (diffs.length === 0) return;
for (const diff of diffs) {
await this.addObject({
storeName: "diffs",
object: diff,
key: null,
});
}
}
public async getDiff(hash: string): Promise<any | null> {
return this.getObject("diffs", hash);
}
public async getAllDiffs(): Promise<Record<string, any>> {
return this.dumpStore("diffs");
}
// ============================================
// BUSINESS METHODS - SECRETS
// ============================================
public async getSharedSecret(address: string): Promise<string | null> {
return this.getObject("shared_secrets", address);
}
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",
object: secret,
key: null,
});
}
}
if (sharedSecrets && sharedSecrets.length > 0) {
for (const { key, value } of sharedSecrets) {
await this.addObject({
storeName: "shared_secrets",
object: value,
key: key,
});
}
}
}
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,
unconfirmed_secrets: Object.values(unconfirmedSecrets),
};
}
}
export default Database;