import EventEmitter from "eventemitter3" ; import { Layerr } from "layerr" ; import isPromise from "is-promise" ; import { ChannelQueue } from "@buttercup/channel-queue" ; import MemoryStorageInterface from "../storage/MemoryStorageInterface" ; import VaultSource from "./VaultSource" ; import StorageInterface from "../storage/StorageInterface" ; import { SetTimeout , VaultSourceID } from "../types" ; export interface InterruptedAutoUpdateFunction { ( ) : void | Promise < any > ; } interface StateChangeEnqueuedFunction { ( ) : void | Promise < any > ; } export interface VaultManagerAddSourceOptions { order ? : number ; } export interface VaultManagerOptions { autoUpdate ? : boolean ; autoUpdateDelay ? : number ; cacheStorage ? : StorageInterface ; sourceStorage ? : StorageInterface ; } const DEFAULT_AUTO_UPDATE_DELAY = 1000 * 60 * 2.5 ; const NOOP = ( ) => { } ; const STORAGE_KEY_PREFIX = "bcup_vaultmgr_" ; const STORAGE_KEY_PREFIX_TEST = /^bcup_vaultmgr_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/ ; augments EventEmitter memberof export default class VaultManager extends EventEmitter { static memberof static STORAGE_KEY_PREFIX = STORAGE_KEY_PREFIX ; _autoUpdateDelay : number ; _autoUpdateEnabled : boolean ; _autoUpdateTimer : SetTimeout = null ; _cacheStorage : StorageInterface ; _initialised : boolean = false ; _queue = new ChannelQueue ( ) ; _sources : Array < VaultSource > = [ ] ; _sourceStorage : StorageInterface ; param opts constructor ( opts = { } ) { super ( ) ; const { autoUpdate = true , autoUpdateDelay = DEFAULT_AUTO_UPDATE_DELAY , cacheStorage = new MemoryStorageInterface ( ) , sourceStorage = new MemoryStorageInterface ( ) } = opts ; this . _cacheStorage = cacheStorage ; this . _sourceStorage = sourceStorage ; this . _autoUpdateEnabled = autoUpdate ; this . _autoUpdateDelay = autoUpdateDelay ; } readonly memberof get sources ( ) : Array < VaultSource > { return [ ... this . _sources ] ; } readonly memberof get unlockedSources ( ) : Array < VaultSource > { return this . _sources . filter ( source => source . status === VaultSource . STATUS_UNLOCKED ) ; } typedef VaultManagerAddSourceOptions property { Number = } order param source param opts memberof async addSource ( source , opts = { } ) { const { order : orderOverride } = opts ; const existing = this . _sources . find ( src => src . id === source . id ) ; if ( existing ) return ; await this . enqueueStateChange ( async ( ) => { source . _vaultManager = this ; const newOrder = typeof orderOverride === "number" ? orderOverride : this . getNextOrder ( ) ; this . _sources . push ( source ) ; source . order = newOrder ; source . on ( "updated" , ( ) => this . dehydrateSource ( source ) ) ; source . on ( "updated" , ( ) => this . emit ( "sourcesUpdated" ) ) ; source . on ( "locked" , ( ) => this . emit ( "sourcesUpdated" ) ) ; source . on ( "unlocked" , ( ) => this . emit ( "sourcesUpdated" ) ) ; const dehydratedString = await source . dehydrate ( ) ; await this . _storeDehydratedSource ( source . id , dehydratedString ) ; this . emit ( "sourcesUpdated" ) ; } ) ; } returns memberof async dehydrate ( ) { await this . enqueueStateChange ( ( ) => Promise . all ( this . _sources . map ( source => this . dehydrateSource ( source ) ) ) ) ; } param sourceID returns memberof async dehydrateSource ( sourceOrSourceID ) { const source = typeof sourceOrSourceID === "string" ? this . getSourceForID ( sourceOrSourceID ) : sourceOrSourceID ; const dehydratedString = await source . dehydrate ( ) ; await this . _storeDehydratedSource ( source . id , dehydratedString ) ; } param cb memberof enqueueStateChange ( cb ) { return this . _queue . channel ( "state" ) . enqueue ( cb ) ; } returns memberof getNextOrder ( ) { return Math . max ( ... this . _sources . map ( source => source . _order ) , - 1 ) + 1 ; } param sourceID returns memberof getSourceForID ( sourceID ) { const source = this . _sources . find ( target => target . id && target . id === sourceID ) ; return source || null ; } memberof getUpdateableSources ( ) { return this . _sources . filter ( source => source . canBeUpdated ( ) ) ; } memberof initialise ( ) { if ( this . _initialised ) return ; this . _startAutoUpdateTimer ( ) ; this . _initialised = true ; } param cb returns memberof example interruptAutoUpdate ( cb ) { return this . enqueueStateChange ( NOOP ) . then ( ( ) => this . _queue . channel ( "autoUpdateInterrupt" ) . enqueue ( ( ) => { const enabled = this . _autoUpdateEnabled ; const delay = this . _autoUpdateDelay ; this . toggleAutoUpdating ( false ) ; const restoreAutoUpdating = ( ) => { this . toggleAutoUpdating ( enabled , delay ) ; } ; let retVal ; try { retVal = cb ( ) ; } catch ( err ) { restoreAutoUpdating ( ) ; throw err ; } if ( ! isPromise ( retVal ) ) { restoreAutoUpdating ( ) ; return retVal ; } return ( < Promise < any > > retVal ) . then ( res => { restoreAutoUpdating ( ) ; return res ; } ) . catch ( err => { restoreAutoUpdating ( ) ; throw err ; } ) ; } ) ) ; } returns memberof throws async rehydrate ( ) { await this . _migrateLegacyVaults ( ) ; const storageKeys = await this . _sourceStorage . getAllKeys ( ) ; await Promise . all ( storageKeys . filter ( key => STORAGE_KEY_PREFIX_TEST . test ( key ) ) . map ( async ( key ) => { const dehydratedSource = await this . _sourceStorage . getValue ( key ) ; const source = VaultSource . rehydrate ( dehydratedSource ) ; await this . addSource ( source , { order : source . order } ) ; } ) ) ; this . reorderSources ( ) ; this . emit ( "sourcesUpdated" ) ; } param sourceID returns memberof async removeSource ( sourceID ) { await this . enqueueStateChange ( async ( ) => { const sourceIndex = this . _sources . findIndex ( source => source . id === sourceID ) ; if ( sourceIndex === - 1 ) { throw new Layerr ( `Failed removing source: No source found for ID: ${ sourceID }` ) ; } const source = this . sources [ sourceIndex ] ; source . removeAllListeners ( ) ; this . _sources . splice ( sourceIndex , 1 ) ; this . emit ( "sourcesUpdated" ) ; await this . _sourceStorage . removeKey ( `${ STORAGE_KEY_PREFIX }${ sourceID }` ) ; } ) ; } param sourceID param position memberof throws async reorderSource ( sourceID , position ) { const source = this . getSourceForID ( sourceID ) ; if ( ! source ) { throw new Layerr ( `Failed reordering source: No source found for ID: ${ sourceID }` ) ; } if ( position === source . _order ) { return ; } const originalOrder = source . _order ; source . _order = position ; const movingUp = position < originalOrder ; this . sources . forEach ( otherSource => { if ( otherSource . id !== sourceID ) { if ( movingUp && otherSource . _order >= position ) { otherSource . _order += 1 ; } else if ( ! movingUp && otherSource . _order <= position ) { otherSource . _order -= 1 ; } } } ) ; this . reorderSources ( ) ; await this . dehydrateSource ( source ) ; } memberof reorderSources ( ) { this . _sources . sort ( ( sourceA , sourceB ) => { if ( sourceA . _order > sourceB . _order ) { return 1 ; } else if ( sourceB . _order > sourceA . _order ) { return - 1 ; } return 0 ; } ) ; this . _sources . forEach ( ( source , index ) => { source . _order = index ; } ) ; this . emit ( "sourcesUpdated" ) ; } param enable param delay memberof toggleAutoUpdating ( enable = ! this . _autoUpdateEnabled , delay = this . _autoUpdateDelay ) { if ( enable ) { this . _autoUpdateDelay = delay ; this . _startAutoUpdateTimer ( ) ; } else { this . _stopAutoUpdateTimer ( ) ; } } _autoUpdateSources ( ) { this . emit ( "autoUpdateStart" ) ; return this . enqueueStateChange ( async ( ) => { const updateableSources = this . getUpdateableSources ( ) ; if ( updateableSources . length <= 0 ) { return ; } await Promise . all ( updateableSources . map ( source => source . update ( ) . catch ( err => { console . error ( `Failed auto-updating source: ${ source . id }` , err ) ; this . emit ( "autoUpdateFailed" , { source , error : err } ) ; } ) ) ) ; this . emit ( "autoUpdateStop" ) ; } ) ; } async _migrateLegacyVaults ( ) { const legacyPrefix = "bcup_archivemgr_" ; const storageKeys = await this . _sourceStorage . getAllKeys ( ) ; await Promise . all ( storageKeys . filter ( key => key && key . indexOf ( legacyPrefix ) === 0 ) . map ( async ( key ) => { const value = await this . _sourceStorage . getValue ( key ) ; if ( ! value ) return ; const newKey = key . replace ( legacyPrefix , STORAGE_KEY_PREFIX ) ; await this . _sourceStorage . setValue ( newKey , value ) ; await this . _sourceStorage . removeKey ( key ) ; } ) ) ; } async _startAutoUpdateTimer ( ) { await this . enqueueStateChange ( ( ) => { clearTimeout ( this . _autoUpdateTimer ) ; this . _autoUpdateTimer = setTimeout ( ( ) => { this . _autoUpdateSources ( ) . then ( ( ) => { if ( this . _autoUpdateEnabled ) { this . _startAutoUpdateTimer ( ) ; } } ) ; } , this . _autoUpdateDelay ) ; } ) ; } async _stopAutoUpdateTimer ( ) { await this . enqueueStateChange ( ( ) => { clearTimeout ( this . _autoUpdateTimer ) ; this . _autoUpdateTimer = null ; } ) ; } _storeDehydratedSource ( id , dehydratedSource ) { return this . _sourceStorage . setValue ( `${ STORAGE_KEY_PREFIX }${ id }` , dehydratedSource ) ; } }