477 lines
14 KiB
TypeScript
Executable File
477 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) => {
|
|
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
|
|
// ============================================
|
|
|
|
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;
|