import { CRUDEngine , error as StoreEngineError } from '@wireapp/store-engine' ; import Dexie , { DexieError } from 'dexie' ; import logdown = require ( 'logdown' ) ; type DexieObservable = { _dbSchema ? : Object ; } ; export class IndexedDBEngine implements CRUDEngine { private db : Dexie & DexieObservable = new Dexie ( '' ) ; private readonly logger : logdown . Logger ; public storeName = '' ; constructor ( ) { this . logger = logdown ( '@wireapp/store-engine-dexie' , { logger : console , markdown : false , } ) ; } private canUseIndexedDB ( ) { const platform = typeof global === 'undefined' ? window : global ; if ( 'indexedDB' in platform ) { return new Promise ( ( resolve , reject ) => { const name = 'test' ; const DBOpenRequest = platform . indexedDB . open ( name ) ; DBOpenRequest . onerror = error => reject ( error ) ; DBOpenRequest . onsuccess = ( ) => { const db = DBOpenRequest . result ; db . close ( ) ; const deleteRequest = platform . indexedDB . deleteDatabase ( name ) ; deleteRequest . onerror = error => reject ( error ) ; deleteRequest . onsuccess = ( ) => resolve ( ) ; } ; } ) ; } return Promise . reject ( new StoreEngineError . UnsupportedError ( 'Could not find indexedDB in global scope' ) ) ; } see https private async hasEnoughQuota ( ) { if ( 'storage' in navigator && 'estimate' in navigator . storage ) { const { quota , usage } = await navigator . storage . estimate ( ) ; if ( typeof quota === 'number' && typeof usage === 'number' ) { const diskIsFull = usage >= quota ; if ( diskIsFull ) { const errorMessage = `Out of disk space. Using "${ usage }" out of "${ quota }" bytes.` ; throw new StoreEngineError . LowDiskSpaceError ( errorMessage ) ; } } } } public async isSupported ( ) { await this . canUseIndexedDB ( ) ; await this . hasEnoughQuota ( ) ; } public async init ( storeName , registerPersisted = false ) { await this . isSupported ( ) ; const dexie = this . assignDb ( new Dexie ( storeName ) ) ; if ( registerPersisted ) { await this . registerPersistentStorage ( ) ; } return dexie ; } public async initWithDb ( db , registerPersisted = false ) { const dexie = this . assignDb ( db ) ; if ( registerPersisted ) { await this . registerPersistentStorage ( ) ; } return dexie ; } public async isStoragePersisted ( ) { if ( navigator . storage ?. persisted ) { const isPersisted = await navigator . storage . persisted ( ) ; return isPersisted ; } return false ; } private assignDb ( db ) { this . db = db ; this . storeName = this . db . name ; return this . db ; } public purge ( ) { return this . db ? this . db . delete ( ) : Dexie . delete ( this . storeName ) ; } private mapDatabaseError < PrimaryKey = string > ( error , tableName , primaryKey ) { const isAlreadyExisting = error instanceof Dexie . ConstraintError ; see https const hasNotEnoughDiskSpace = error . name === Dexie . errnames . QuotaExceeded || error . inner ?. name === Dexie . errnames . QuotaExceeded ; if ( isAlreadyExisting ) { const message = `Record "${ primaryKey }" already exists in "${ tableName }". You need to delete the record first if you want to overwrite it.` ; return new StoreEngineError . RecordAlreadyExistsError ( message ) ; } else if ( hasNotEnoughDiskSpace ) { const message = `Cannot save "${ primaryKey }" in "${ tableName }" because there is low disk space.` ; return new StoreEngineError . LowDiskSpaceError ( message ) ; } return error ; } public async create < EntityType = Object , PrimaryKey = string > ( tableName , primaryKey , entity ) { if ( entity ) { try { const newPrimaryKey = await this . db . table < EntityType , PrimaryKey > ( tableName ) . add ( entity , primaryKey ) ; return newPrimaryKey ; } catch ( error ) { throw this . mapDatabaseError ( error as DexieError , tableName , primaryKey ) ; } } const message = `Record "${ primaryKey }" cannot be saved in "${ tableName }" because it's "undefined" or "null".` ; throw new StoreEngineError . RecordTypeError ( message ) ; } public async delete < PrimaryKey = string > ( tableName , primaryKey ) { await this . db . table < any , PrimaryKey > ( tableName ) . delete ( primaryKey ) ; return primaryKey ; } public async deleteAll ( tableName ) { await this . db . table ( tableName ) . clear ( ) ; return true ; } public async clearTables ( ) { const tableNames = this . db . tables . map ( table => table . name ) ; await Promise . all ( tableNames . map ( tableName => this . deleteAll ( tableName ) ) ) ; } public async read < EntityType = Object , PrimaryKey = string > ( tableName , primaryKey ) { const record = await this . db . table < EntityType > ( tableName ) . get ( primaryKey ) ; if ( record ) { return record ; } const message = `Record "${ primaryKey }" in "${ tableName }" could not be found.` ; throw new StoreEngineError . RecordNotFoundError ( message ) ; } public readAll < EntityType > ( tableName ) { return this . db . table ( tableName ) . toArray ( ) ; } public async readAllPrimaryKeys < PrimaryKey = string > ( tableName ) { const keys = await this . db . table < PrimaryKey > ( tableName ) . toCollection ( ) . keys ( ) ; return keys . map ( key => key as any as PrimaryKey ) ; } see https see https public async registerPersistentStorage ( ) { if ( ! navigator || ! navigator . storage || ! navigator . storage . persist ) { return false ; } const granted = await navigator . storage . persist ( ) ; if ( granted ) { this . logger . info ( 'Storage will not be cleared except by explicit user action' ) ; return true ; } this . logger . info ( 'Storage may be cleared by the UA under storage pressure.' ) ; return false ; } public async update < PrimaryKey = string > ( tableName , primaryKey , changes ) { const updatedRecords = await this . db . table ( tableName ) . update ( primaryKey , changes ) ; if ( updatedRecords === 0 ) { const message = `Record "${ primaryKey }" in "${ tableName }" could not be found.` ; throw new StoreEngineError . RecordNotFoundError ( message ) ; } return primaryKey ; } public updateOrCreate < PrimaryKey = string , ChangesType = Object > ( tableName , primaryKey , changes ) { return this . db . table < ChangesType , PrimaryKey > ( tableName ) . put ( changes , primaryKey ) ; } }