455 lines
16 KiB
TypeScript
Executable File
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;
|