295 lines
9.7 KiB
TypeScript
Executable File
295 lines
9.7 KiB
TypeScript
Executable File
import Services from './service';
|
|
|
|
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 = new MessageChannel();
|
|
private messageChannelForGet: MessageChannel = new MessageChannel();
|
|
private storeDefinitions = {
|
|
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 } },
|
|
],
|
|
},
|
|
};
|
|
|
|
// 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 = () => {
|
|
setTimeout(() => {
|
|
this.db = request.result;
|
|
this.initServiceWorker();
|
|
resolve();
|
|
}, 500);
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
private createMessageChannel(responseHandler: (event: MessageEvent) => void): MessageChannel {
|
|
const messageChannel = new MessageChannel();
|
|
messageChannel.port1.onmessage = responseHandler;
|
|
return messageChannel;
|
|
}
|
|
|
|
private async initServiceWorker() {
|
|
if ('serviceWorker' in navigator) {
|
|
try {
|
|
const registration = await navigator.serviceWorker.register('/src/service-workers/database.worker.js', { type: 'module' });
|
|
console.log('Service Worker registered with scope:', registration.scope);
|
|
|
|
this.serviceWorkerRegistration = registration
|
|
await this.checkForUpdates();
|
|
|
|
// Set up the message channels
|
|
this.messageChannel.port1.onmessage = this.handleAddObjectResponse;
|
|
this.messageChannelForGet.port1.onmessage = this.handleGetObjectResponse;
|
|
registration.active?.postMessage(
|
|
{
|
|
type: 'START',
|
|
},
|
|
[this.messageChannel.port2],
|
|
);
|
|
// Optionally, initialize service worker with some data
|
|
} catch (error) {
|
|
console.error('Service Worker registration failed:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 handleAddObjectResponse = async (event: MessageEvent) => {
|
|
const data = event.data;
|
|
console.log('Received response from service worker (ADD_OBJECT):', data);
|
|
if (data.type === 'NOTIFICATIONS') {
|
|
const service = await Services.getInstance();
|
|
service.setNotifications(data.data);
|
|
}
|
|
};
|
|
|
|
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((resolve, reject) => {
|
|
// Check if the service worker is active
|
|
if (!this.serviceWorkerRegistration?.active) {
|
|
reject(new Error('Service worker is not active'));
|
|
return;
|
|
}
|
|
|
|
// 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 {
|
|
this.serviceWorkerRegistration.active.postMessage(
|
|
{
|
|
type: 'ADD_OBJECT',
|
|
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 {
|
|
// Wait for both getAllKeys() and getAll() to resolve
|
|
const [keys, values] = await Promise.all([
|
|
new Promise<any[]>((resolve, reject) => {
|
|
const request = store.getAllKeys();
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
}),
|
|
new Promise<any[]>((resolve, reject) => {
|
|
const request = store.getAll();
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
}),
|
|
]);
|
|
|
|
// Combine keys and values into an object
|
|
const result: Record<string, any> = Object.fromEntries(keys.map((key, index) => [key, values[index]]));
|
|
return result;
|
|
} 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;
|