import utf8 from 'utf8' ; import { isIndexedDbValid } from './indexedDb/isIndexedDbValid' ; import { IStorageAdapter , IStorageAdapterConfig } from './IStorageAdapter' ; import { StorageEntryAlreadyExistsError } from './errors' ; const READ_ONLY = 'readonly' ; const READ_WRITE = 'readwrite' ; type DeferredOperation = { resolve ( val ? ) : void ; reject ( err ) : void ; promise : Promise < any > ; } ; type DbContext = { db : IDBDatabase | null ; dbReady : Promise < any > | null ; deferredOperations : DeferredOperation [ ] ; } ; type DbInfo = { db : IDBDatabase | null ; name : string ; version ? : number ; storeName : string ; } ; const dbContexts = { } ; /**
 * Implementation of  IStorageAdapter that uses IndexedDB for
 * persistence. For use in browsers.
  export default class IndexedDbStorageAdapter implements IStorageAdapter { private _dbInfo ? : DbInfo ; private _defaultConfig : { name : string ; version : number ; storeName : string ; } ; private _ready : Promise < void > ; param { IStorageAdapterConfig } config constructor ( config ) { if ( ! isIndexedDbValid ( ) ) { throw new Error ( 'Cannot use IndexedDbStorageAdapter. indexedDb is not supported' ) ; } this . _defaultConfig = { name : config . name , version : 1 , storeName : 'keyvaluepairs' } ; this . _ready = this . _initStorage ( ) ; } inheritDoc store ( key , data ) { key = normalizeKey ( key ) ; return new Promise ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_WRITE , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const req = store . add ( data , key ) ; transaction ! . oncomplete = ( ) => { resolve ( ) ; } ; transaction ! . onabort = transaction ! . onerror = ( ) => { let error = req . error ? req . error : req . transaction ! . error ; if ( error && error . name === 'ConstraintError' ) { reject ( new StorageEntryAlreadyExistsError ( key ) ) ; } reject ( error ) ; } ; } catch ( error ) { reject ( error ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } inheritDoc load ( key ) { key = normalizeKey ( key ) ; return new Promise ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_ONLY , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const req = store . get ( key ) ; req . onsuccess = function ( ) { if ( req . result == null ) { return resolve ( null ) ; } resolve ( convertDbValue ( req . result ) ) ; } ; req . onerror = function ( ) { reject ( req . error ) ; } ; } catch ( e ) { reject ( e ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } inheritDoc exists ( key ) { key = normalizeKey ( key ) ; return new Promise < boolean > ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_ONLY , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const req = store . openCursor ( key ) ; req . onsuccess = ( ) => { const cursor = req . result ; resolve ( cursor !== null ) ; } ; req . onerror = ( ) => { reject ( req . error ) ; } ; } catch ( e ) { reject ( e ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } inheritDoc remove ( key ) { key = normalizeKey ( key ) ; return new Promise < boolean > ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_WRITE , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const countReq = store . count ( key ) ; let delReq ; countReq . onsuccess = ( ) => { const count = countReq . result ; if ( count === 0 ) { return resolve ( false ) ; } delReq = store [ 'delete' ] ( key ) ; delReq . onsuccess = ( ) => resolve ( true ) ; } ; transaction ! . onabort = transaction ! . onerror = ( ) => { const req = delReq || countReq ; const err = req . error ? req . error : req . transaction ! . error ; reject ( err ) ; } ; } catch ( e ) { reject ( e ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } inheritDoc update ( key , data ) { key = normalizeKey ( key ) ; return new Promise ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_WRITE , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const req = store . put ( data , key ) ; req . onsuccess = ( ) => { resolve ( ) ; } ; req . onerror = ( ) => { reject ( req . error ) ; } ; } catch ( error ) { reject ( error ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } inheritDoc clear ( ) { return new Promise < void > ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_WRITE , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const req = store . clear ( ) ; transaction ! . oncomplete = ( ) => resolve ( ) ; transaction ! . onabort = transaction ! . onerror = ( ) => { const err = req . error ? req . error : req . transaction ! . error ; reject ( err ) ; } ; } catch ( e ) { reject ( e ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } inheritDoc list ( ) { return new Promise ( ( resolve , reject ) => { this . ready ( ) . then ( ( ) => { createTransaction ( this . _dbInfo ! , READ_ONLY , ( err , transaction ) => { if ( err ) { return reject ( err ) ; } try { const store = transaction ! . objectStore ( this . _dbInfo ! . storeName ! ) ; const req = store . openCursor ( ) ; const entries = [ ] ; req . onsuccess = ( ) => { const cursor = req . result ; if ( ! cursor ) { resolve ( entries ) ; } else { entries . push ( convertDbValue ( cursor . value ) ) ; cursor . continue ( ) ; } } ; req . onerror = ( ) => { reject ( req . error ) ; } ; } catch ( e ) { reject ( e ) ; } } ) ; } ) . catch ( reject ) ; } ) ; } private _initStorage = ( ) => { const dbInfo = { db : null , name : this . _defaultConfig . name , storeName : this . _defaultConfig . storeName , version : this . _defaultConfig . version } ; let dbContext = dbContexts [ dbInfo . name ] ; if ( ! dbContext ) { dbContext = createDbContext ( ) ; dbContexts [ dbInfo . name ] = dbContext ; } return Promise . resolve ( ) . then ( ( ) => { dbInfo . db = dbContext . db ; return _getOriginalConnection ( dbInfo ) ; } ) . then ( db => { dbInfo . db = db ; if ( _isUpgradeNeeded ( dbInfo , this . _defaultConfig . version ) ) { return _getUpgradedConnection ( dbInfo ) ; } return db ; } ) . then ( db => { dbInfo . db = dbContext . db = db ; this . _dbInfo = dbInfo ; } ) ; } ; private ready = ( ) => { const promise = this . _ready . then ( ( ) => { const dbContext = dbContexts [ this . _dbInfo ! . name ] ; if ( dbContext && dbContext . dbReady ) { return dbContext . dbReady ; } } ) ; return promise ; } ; } function createDbContext ( ) { return { db : null , dbReady : null , deferredOperations : [ ] } ; } function _deferReadiness ( dbInfo ) { const dbContext = dbContexts [ dbInfo . name ] ; const deferredOperation = { } as DeferredOperation ; deferredOperation . promise = new Promise ( ( resolve , reject ) => { deferredOperation . resolve = resolve ; deferredOperation . reject = reject ; } ) ; dbContext . deferredOperations . push ( deferredOperation ) ; if ( ! dbContext . dbReady ) { dbContext . dbReady = deferredOperation . promise ; } else { dbContext . dbReady = dbContext . dbReady . then ( ( ) => deferredOperation . promise ) ; } } function _advanceReadiness ( dbInfo ) { const dbContext = dbContexts [ dbInfo . name ] ; const deferredOperation = dbContext . deferredOperations . pop ( ) ; if ( deferredOperation ) { deferredOperation . resolve ( ) ; return deferredOperation . promise ; } } function _rejectReadiness ( dbInfo , err ) { const dbContext = dbContexts [ dbInfo . name ] ; const deferredOperation = dbContext . deferredOperations . pop ( ) ; if ( deferredOperation ) { deferredOperation . reject ( err ) ; return deferredOperation . promise ; } } function _getConnection ( dbInfo , upgradeNeeded ) { return new Promise ( function ( resolve , reject ) { dbContexts [ dbInfo . name ] = dbContexts [ dbInfo . name ] || createDbContext ( ) ; if ( dbInfo . db ) { if ( upgradeNeeded ) { _deferReadiness ( dbInfo ) ; dbInfo . db . close ( ) ; } else { return resolve ( dbInfo . db ) ; } } const dbArgs = [ dbInfo . name ] ; if ( upgradeNeeded ) { dbArgs . push ( String ( dbInfo . version ) ) ; } const openReq = indexedDB . open . apply ( indexedDB , dbArgs ) ; if ( upgradeNeeded ) { openReq . onupgradeneeded = ( e ) => { const db = openReq . result ; try { db . createObjectStore ( dbInfo . storeName ) ; } catch ( ex ) { if ( ex . name === 'ConstraintError' ) { console . warn ( 'The database "' + dbInfo . name + '"' + ' has been upgraded from version ' + e . oldVersion + ' to version ' + e . newVersion + ', but the storage "' + dbInfo . storeName + '" already exists.' ) ; } else { throw ex ; } } } ; } openReq . onerror = ( e ) => { e . preventDefault ( ) ; reject ( openReq . error ) ; } ; openReq . onsuccess = ( ) => { resolve ( openReq . result ) ; _advanceReadiness ( dbInfo ) ; } ; } ) ; } function _getOriginalConnection ( dbInfo ) { return _getConnection ( dbInfo , false ) ; } function _getUpgradedConnection ( dbInfo ) { return _getConnection ( dbInfo , true ) ; } function _isUpgradeNeeded ( dbInfo , defaultVersion ? ) { if ( ! dbInfo . db ) { return true ; } const isNewStore = ! dbInfo . db . objectStoreNames . contains ( dbInfo . storeName ! ) ; const isDowngrade = dbInfo . version ! < dbInfo . db . version ; const isUpgrade = dbInfo . version ! > dbInfo . db . version ; if ( isDowngrade ) { if ( dbInfo . version !== defaultVersion ) { console . warn ( 'The database "' + dbInfo . name + '"' + " can't be downgraded from version " + dbInfo . db . version + ' to version ' + dbInfo . version + '.' ) ; } dbInfo . version = dbInfo . db . version ; } if ( isUpgrade || isNewStore ) { if ( isNewStore ) { const incVersion = dbInfo . db . version + 1 ; if ( incVersion > dbInfo . version ! ) { dbInfo . version = incVersion ; } } return true ; } return false ; } function _tryReconnect ( dbInfo ) { _deferReadiness ( dbInfo ) ; const dbContext = dbContexts [ dbInfo . name ] ; dbInfo . db = null ; return _getOriginalConnection ( dbInfo ) . then ( db => { dbInfo . db = db ; if ( _isUpgradeNeeded ( dbInfo ) ) { return _getUpgradedConnection ( dbInfo ) ; } return db ; } ) . then ( db => { dbInfo . db = dbContext . db = db ; } ) . catch ( err => { _rejectReadiness ( dbInfo , err ) ; throw err ; } ) ; } function createTransaction ( dbInfo , mode , callback , retries ? ) { if ( retries === undefined ) { retries = 1 ; } try { const tx = dbInfo . db ! . transaction ( dbInfo . storeName ! , mode ) ; callback ( null , tx ) ; } catch ( err ) { if ( retries > 0 && ( ! dbInfo . db || err . name === 'InvalidStateError' || err . name === 'NotFoundError' ) ) { Promise . resolve ( ) . then ( ( ) => { if ( ! dbInfo . db || ( err . name === 'NotFoundError' && ! dbInfo . db . objectStoreNames . contains ( dbInfo . storeName ! ) && ( dbInfo . version as number ) <= dbInfo . db . version ) ) { if ( dbInfo . db ) { dbInfo . version = dbInfo . db . version + 1 ; } return _getUpgradedConnection ( dbInfo ) as Promise < any > ; } } ) . then ( ( ) => { return _tryReconnect ( dbInfo ) . then ( function ( ) { createTransaction ( dbInfo , mode , callback , retries ! - 1 ) ; } ) ; } ) . catch ( callback ) ; } callback ( err ) ; } } function normalizeKey ( key ) { if ( typeof key !== 'string' ) { console . warn ( `${ key } used as a key, but it is not a string.` ) ; key = String ( key ) ; } return key ; } function convertDbValue ( value ) { if ( value instanceof ArrayBuffer ) { const uint8Array = new Uint8Array ( value ) ; return utf8 . decode ( String . fromCharCode . apply ( null , uint8Array ) ) ; } if ( typeof value === 'string' ) { return value ; } throw new TypeError ( 'Unknown value format' ) ; }