More TypeScript
This commit is contained in:
219
app/assets/javascripts/database.ts
Normal file
219
app/assets/javascripts/database.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { WebApplication } from './application';
|
||||
import { SNAlertService } from "../../../../snjs/dist/@types";
|
||||
|
||||
const DB_NAME = 'standardnotes';
|
||||
const STORE_NAME = 'items';
|
||||
const READ_WRITE = 'readwrite';
|
||||
|
||||
const OUT_OF_SPACE =
|
||||
'Unable to save changes locally because your device is out of space. ' +
|
||||
'Please free up some disk space and try again, otherwise, your data may end ' +
|
||||
'up in an inconsistent state.';
|
||||
|
||||
const DB_DELETION_BLOCKED =
|
||||
'Your browser is blocking Standard Notes from deleting the local database. ' +
|
||||
'Make sure there are no other open windows of this app and try again. ' +
|
||||
'If the issue persists, please manually delete app data to sign out.';
|
||||
|
||||
const QUOTE_EXCEEDED_ERROR = 'QuotaExceededError';
|
||||
|
||||
export class Database {
|
||||
|
||||
private locked = true
|
||||
private alertService?: SNAlertService
|
||||
private db?: IDBDatabase
|
||||
|
||||
public deinit() {
|
||||
this.alertService = undefined;
|
||||
this.db = undefined;
|
||||
}
|
||||
|
||||
public setAlertService(alertService: SNAlertService) {
|
||||
this.alertService = alertService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relinquishes the lock and allows db operations to proceed
|
||||
*/
|
||||
public unlock() {
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the database natively, or returns the existing database object if already opened.
|
||||
* @param onNewDatabase - Callback to invoke when a database has been created
|
||||
* as part of the open process. This can happen on new application sessions, or if the
|
||||
* browser deleted the database without the user being aware.
|
||||
*/
|
||||
public async openDatabase(onNewDatabase?: () => void): Promise<IDBDatabase | undefined> {
|
||||
if (this.locked) {
|
||||
throw Error('Attempting to open locked database');
|
||||
}
|
||||
if (this.db) {
|
||||
return this.db;
|
||||
}
|
||||
const request = window.indexedDB.open(DB_NAME, 1);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onerror = (event) => {
|
||||
const target = event!.target! as any;
|
||||
if (target.errorCode) {
|
||||
this.showAlert('Offline database issue: ' + target.errorCode);
|
||||
} else {
|
||||
this.displayOfflineAlert();
|
||||
}
|
||||
reject(new Error('Unable to open db'));
|
||||
};
|
||||
request.onblocked = (event) => {
|
||||
reject(Error('IndexedDB open request blocked'));
|
||||
};
|
||||
request.onsuccess = (event) => {
|
||||
const target = event!.target! as IDBOpenDBRequest;
|
||||
const db = target.result;
|
||||
db.onversionchange = () => {
|
||||
db.close();
|
||||
};
|
||||
db.onerror = (errorEvent) => {
|
||||
const target = errorEvent?.target as any;
|
||||
throw Error('Database error: ' + target.errorCode);
|
||||
};
|
||||
this.db = db;
|
||||
resolve(db);
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const target = event!.target! as IDBOpenDBRequest;
|
||||
const db = target.result;
|
||||
db.onversionchange = () => {
|
||||
db.close();
|
||||
};
|
||||
/* Create an objectStore for this database */
|
||||
const objectStore = db.createObjectStore(
|
||||
STORE_NAME,
|
||||
{ keyPath: 'uuid' }
|
||||
);
|
||||
objectStore.createIndex(
|
||||
'uuid',
|
||||
'uuid',
|
||||
{ unique: true }
|
||||
);
|
||||
objectStore.transaction.oncomplete = () => {
|
||||
/* Ready to store values in the newly created objectStore. */
|
||||
if (db.version === 1 && onNewDatabase) {
|
||||
onNewDatabase && onNewDatabase();
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllPayloads() {
|
||||
const db = (await this.openDatabase())!;
|
||||
return new Promise((resolve) => {
|
||||
const objectStore =
|
||||
db.transaction(STORE_NAME).
|
||||
objectStore(STORE_NAME);
|
||||
const payloads: any = [];
|
||||
const cursorRequest = objectStore.openCursor();
|
||||
cursorRequest.onsuccess = (event) => {
|
||||
const target = event!.target! as any;
|
||||
const cursor = target.result;
|
||||
if (cursor) {
|
||||
payloads.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(payloads);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async savePayload(payload: any) {
|
||||
return this.savePayloads([payload]);
|
||||
}
|
||||
|
||||
public async savePayloads(payloads: any[]) {
|
||||
if (payloads.length === 0) {
|
||||
return;
|
||||
}
|
||||
const db = (await this.openDatabase())!;
|
||||
const transaction = db.transaction(STORE_NAME, READ_WRITE);
|
||||
return new Promise((resolve, reject) => {
|
||||
transaction.oncomplete = () => { };
|
||||
transaction.onerror = (event) => {
|
||||
const target = event!.target! as any;
|
||||
this.showGenericError(target.error);
|
||||
};
|
||||
transaction.onabort = (event) => {
|
||||
const target = event!.target! as any;
|
||||
const error = target.error;
|
||||
if (error.name === QUOTE_EXCEEDED_ERROR) {
|
||||
this.showAlert(OUT_OF_SPACE);
|
||||
} else {
|
||||
this.showGenericError(error);
|
||||
}
|
||||
reject(error);
|
||||
};
|
||||
const objectStore = transaction.objectStore(STORE_NAME);
|
||||
this.putItems(objectStore, payloads).then(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private putItems(objectStore: IDBObjectStore, items: any[]) {
|
||||
return Promise.all(items.map((item) => {
|
||||
return new Promise((resolve) => {
|
||||
const request = objectStore.put(item);
|
||||
request.onerror = resolve;
|
||||
request.onsuccess = resolve;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public async deletePayload(uuid: string) {
|
||||
const db = (await this.openDatabase())!;
|
||||
return new Promise((resolve, reject) => {
|
||||
const request =
|
||||
db.transaction(STORE_NAME, READ_WRITE)
|
||||
.objectStore(STORE_NAME)
|
||||
.delete(uuid);
|
||||
request.onsuccess = resolve;
|
||||
request.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
public async clearAllPayloads() {
|
||||
const deleteRequest = window.indexedDB.deleteDatabase(DB_NAME);
|
||||
return new Promise((resolve, reject) => {
|
||||
deleteRequest.onerror = () => {
|
||||
reject(Error('Error deleting database.'));
|
||||
};
|
||||
deleteRequest.onsuccess = () => {
|
||||
this.db = undefined;
|
||||
resolve();
|
||||
};
|
||||
deleteRequest.onblocked = (event) => {
|
||||
this.showAlert(DB_DELETION_BLOCKED);
|
||||
reject(Error('Delete request blocked'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private showAlert(message: string) {
|
||||
this.alertService!.alert(message);
|
||||
}
|
||||
|
||||
private showGenericError(error: {code: number, name: string}) {
|
||||
const message =
|
||||
`Unable to save changes locally due to an unknown system issue. ` +
|
||||
`Issue Code: ${error.code} Issue Name: ${error.name}.`;
|
||||
this.showAlert(message);
|
||||
}
|
||||
|
||||
private displayOfflineAlert() {
|
||||
const message =
|
||||
"There was an issue loading your offline database. This could happen for two reasons:" +
|
||||
"\n\n1. You're in a private window in your browser. We can't save your data without " +
|
||||
"access to the local database. Please use a non-private window." +
|
||||
"\n\n2. You have two windows of the app open at the same time. " +
|
||||
"Please close any other app instances and reload the page.";
|
||||
this.alertService!.alert(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user