ihm_client/src/services/database.service.ts
2025-07-08 17:24:14 +02:00

455 lines
16 KiB
TypeScript
Executable File

import Services from './service';
export class Database {
private static instance: Database;
private db: IDBDatabase | null = null;
private dbName: string = '4nk';
private dbVersion: number = 1;
private serviceWorkerRegistration: ServiceWorkerRegistration | null = null;
private messageChannel: MessageChannel | null = null;
private messageChannelForGet: MessageChannel | null = null;
private serviceWorkerCheckIntervalId: number | null = null;
private storeDefinitions = {
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: [],
},
};
// Private constructor to prevent direct instantiation from outside
private constructor() {}
// Method to access the singleton instance of Database
public static async getInstance(): Promise<Database> {
if (!Database.instance) {
Database.instance = new Database();
await Database.instance.init();
}
return Database.instance;
}
// Initialize the database
private async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onupgradeneeded = () => {
const db = request.result;
Object.values(this.storeDefinitions).forEach(({ name, options, indices }) => {
if (!db.objectStoreNames.contains(name)) {
let store = db.createObjectStore(name, options as IDBObjectStoreParameters);
indices.forEach(({ name, keyPath, options }) => {
store.createIndex(name, keyPath, options);
});
}
});
};
request.onsuccess = async () => {
this.db = request.result;
resolve();
};
request.onerror = () => {
console.error('Database error:', request.error);
reject(request.error);
};
});
}
public async getDb(): Promise<IDBDatabase> {
if (!this.db) {
await this.init();
}
return this.db!;
}
public getStoreList(): { [key: string]: string } {
const objectList: { [key: string]: string } = {};
Object.keys(this.storeDefinitions).forEach((key) => {
objectList[key] = this.storeDefinitions[key as keyof typeof this.storeDefinitions].name;
});
return objectList;
}
public async registerServiceWorker(path: string) {
if (!('serviceWorker' in navigator)) return; // Ensure service workers are supported
console.log('registering worker at', path);
try {
// Get existing service worker registrations
const registrations = await navigator.serviceWorker.getRegistrations();
if (registrations.length === 0) {
// No existing workers: register a new one.
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
} else if (registrations.length === 1) {
// One existing worker: update it (restart it) without unregistering.
this.serviceWorkerRegistration = registrations[0];
await this.serviceWorkerRegistration.update();
console.log('Service Worker updated');
} else {
// More than one existing worker: unregister them all and register a new one.
console.log('Multiple Service Worker(s) detected. Unregistering all...');
await Promise.all(registrations.map(reg => reg.unregister()));
console.log('All previous Service Workers unregistered.');
this.serviceWorkerRegistration = await navigator.serviceWorker.register(path, { type: 'module' });
console.log('Service Worker registered with scope:', this.serviceWorkerRegistration.scope);
}
await this.checkForUpdates();
// Set up a global message listener for responses from the service worker.
navigator.serviceWorker.addEventListener('message', async (event) => {
console.log('Received message from service worker:', event.data);
await this.handleServiceWorkerMessage(event.data);
});
// Set up a periodic check to ensure the service worker is active and to send a SCAN message.
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('Service Worker registration failed:', error);
}
}
// Helper function to wait for service worker activation
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() {
if (this.serviceWorkerRegistration) {
// Check for updates to the service worker
try {
await this.serviceWorkerRegistration.update();
// If there's a new worker waiting, activate it immediately
if (this.serviceWorkerRegistration.waiting) {
this.serviceWorkerRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
} catch (error) {
console.error('Error checking for service worker updates:', error);
}
}
}
private async handleServiceWorkerMessage(message: any) {
switch (message.type) {
case 'TO_DOWNLOAD':
await this.handleDownloadList(message.data);
break;
default:
console.warn('Unknown message type received from service worker:', message);
}
}
private async handleDownloadList(downloadList: string[]): Promise<void> {
// Download the missing data
let requestedStateId: string[] = [];
const service = await Services.getInstance();
for (const hash of downloadList) {
const diff = await service.getDiffByValue(hash);
if (!diff) {
// This should never happen
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) {
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
document.dispatchEvent(new CustomEvent('newDataReceived', {
detail: {
processId,
stateId,
hash,
}
}));
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
if (!requestedStateId.includes(stateId)) {
await service.requestDataFromPeers(processId, [stateId], [roles]);
requestedStateId.push(stateId);
}
}
} catch (e) {
console.error(e);
}
}
}
private handleAddObjectResponse = async (event: MessageEvent) => {
const data = event.data;
console.log('Received response from service worker (ADD_OBJECT):', data);
const service = await Services.getInstance();
if (data.type === 'NOTIFICATIONS') {
service.setNotifications(data.data);
} else if (data.type === 'TO_DOWNLOAD') {
console.log(`Received missing data ${data}`);
// Download the missing data
let requestedStateId: string[] = [];
for (const hash of data.data) {
try {
const valueBytes = await service.fetchValueFromStorage(hash);
if (valueBytes) {
// Save data to db
const blob = new Blob([valueBytes], {type: "application/octet-stream"});
await service.saveBlobToDb(hash, blob);
} else {
// We first request the data from managers
console.log('Request data from managers of the process');
// get the diff from db
const diff = await service.getDiffByValue(hash);
if (diff === null) {
continue;
}
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);
}
}
} catch (e) {
console.error(e);
}
}
}
};
private handleGetObjectResponse = (event: MessageEvent) => {
console.log('Received response from service worker (GET_OBJECT):', event.data);
};
public addObject(payload: { storeName: string; object: any; key: any }): Promise<void> {
return new Promise(async (resolve, reject) => {
// Check if the service worker is active
if (!this.serviceWorkerRegistration) {
// console.warn('Service worker registration is not ready. Waiting...');
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
}
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
// Create a message channel for communication
const messageChannel = new MessageChannel();
// Handle the response from the service worker
messageChannel.port1.onmessage = (event) => {
if (event.data.status === 'success') {
resolve();
} else {
const error = event.data.message;
reject(new Error(error || 'Unknown error occurred while adding object'));
}
};
// Send the add object request to the service worker
try {
activeWorker?.postMessage(
{
type: 'ADD_OBJECT',
payload,
},
[messageChannel.port2],
);
} catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`));
}
});
}
public batchWriting(payload: { storeName: string; objects: { key: any; object: any }[] }): Promise<void> {
return new Promise(async (resolve, reject) => {
if (!this.serviceWorkerRegistration) {
this.serviceWorkerRegistration = await navigator.serviceWorker.ready;
}
const activeWorker = await this.waitForServiceWorkerActivation(this.serviceWorkerRegistration);
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data.status === 'success') {
resolve();
} else {
const error = event.data.message;
reject(new Error(error || 'Unknown error occurred while adding objects'));
}
};
try {
activeWorker?.postMessage(
{
type: 'BATCH_WRITING',
payload,
},
[messageChannel.port2],
);
} catch (error) {
reject(new Error(`Failed to send message to service worker: ${error}`));
}
});
}
public async getObject(storeName: string, key: string): Promise<any | null> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const result = await new Promise((resolve, reject) => {
const getRequest = store.get(key);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
return result;
}
public async dumpStore(storeName: string): Promise<Record<string, any>> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
try {
return new Promise((resolve, reject) => {
const result: Record<string, any> = {};
const cursor = store.openCursor();
cursor.onsuccess = (event) => {
const request = event.target as IDBRequest<IDBCursorWithValue | null>;
const cursor = request.result;
if (cursor) {
result[cursor.key as string] = cursor.value;
cursor.continue();
} else {
resolve(result);
}
};
cursor.onerror = () => {
reject(cursor.error);
};
});
} catch (error) {
console.error('Error fetching data from IndexedDB:', error);
throw error;
}
}
public async deleteObject(storeName: string, key: string): Promise<void> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
try {
await new Promise((resolve, reject) => {
const getRequest = store.delete(key);
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = () => reject(getRequest.error);
});
} catch (e) {
throw e;
}
}
public async clearStore(storeName: string): Promise<void> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
try {
await new Promise((resolve, reject) => {
const clearRequest = store.clear();
clearRequest.onsuccess = () => resolve(clearRequest.result);
clearRequest.onerror = () => reject(clearRequest.error);
});
} catch (e) {
throw e;
}
}
// Request a store by index
public async requestStoreByIndex(storeName: string, indexName: string, request: string): Promise<any[]> {
const db = await this.getDb();
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
const index = store.index(indexName);
try {
return new Promise((resolve, reject) => {
const getAllRequest = index.getAll(request);
getAllRequest.onsuccess = () => {
const allItems = getAllRequest.result;
const filtered = allItems.filter(item => item.state_id === request);
resolve(filtered);
};
getAllRequest.onerror = () => reject(getAllRequest.error);
});
} catch (e) {
throw e;
}
}
}
export default Database;