Source: sources/managers/TDataBaseManager.js

/**
 * @author [Tristan Valcke]{@link https://github.com/Itee}
 * @license [BSD-3-Clause]{@link https://opensource.org/licenses/BSD-3-Clause}
 *
 * @class TDataBaseManager
 * @classdesc The base class of database managers. Give the basic interface about database call.
 *
 * @requires {@link HttpVerb}
 * @requires {@link ResponseType}
 * @requires {@link HttpStatusCode}
 * @requires {@link TOrchestrator}
 * @requires {@link TStore}
 *
 * @example Todo
 *
 */

/* eslint-env browser */

import {
    DefaultLogger,
    TLogger
}                 from 'itee-core'
import { toEnum } from 'itee-utils'
import {
    isArray,
    isArrayOfSingleElement,
    isBlankString,
    isDefined,
    isEmptyObject,
    isEmptyString,
    isNotBlankString,
    isNotDefined,
    isNotEmptyArray,
    isNotEmptyObject,
    isNotEmptyString,
    isNotNumber,
    isNotObject,
    isNotString,
    isNull,
    isNumberNegative,
    isNumberPositive,
    isObject,
    isString,
    isUndefined,
    isZero
}                 from 'itee-validators'
import {
    HttpStatusCode,
    HttpVerb,
    ResponseType
}                 from '../cores/TConstants'
import { TStore } from '../cores/TStore'


/**
 * @deprecated
 */
class IdGenerator {

    constructor () {
        this._id = 0
    }

    get id () {
        this._id += 1
        return this._id
    }

}

const Generate = new IdGenerator()

/**
 *
 * @type {ReadonlyArray<unknown>}
 */
const RequestType = toEnum( {
    CreateOne:   0,
    CreateMany:  1,
    ReadOne:     2,
    ReadMany:    3,
    ReadWhere:   4,
    ReadAll:     5,
    UpdateOne:   6,
    UpdateMany:  7,
    UpdateWhere: 8,
    UpdateAll:   9,
    DeleteOne:   10,
    DeleteMany:  11,
    DeleteWhere: 12,
    DeleteAll:   13
} )

/**
 * @class
 */
class TDataBaseManager {

    /**
     *
     * @returns {number}
     */
    static get requestId () {
        TDataBaseManager._requestId++
        return TDataBaseManager._requestId
    }
    /**
     *
     * @param parameters
     */
    constructor ( parameters = {} ) {

        const _parameters = {
            ...{
                basePath:               '/',
                responseType:           ResponseType.Json,
                bunchSize:              500,
                requestAggregationTime: 200,
                requestsConcurrency:    6,
                logger:                 DefaultLogger
            }, ...parameters
        }

        this.basePath               = _parameters.basePath
        this.responseType           = _parameters.responseType
        this.bunchSize              = _parameters.bunchSize
        this.requestAggregationTime = _parameters.requestAggregationTime
        this.requestsConcurrency    = _parameters.requestsConcurrency
        this.logger                 = _parameters.logger

        this._cache                = new TStore()
        this._waitingQueue         = []
        this._aggregateQueue       = []
        this._requestQueue         = []
        this._processQueue         = []
        this._aggregationTimeoutId = null

        this._idToRequest = []

    }
    /**
     *
     * @returns {*}
     */
    get basePath () {
        return this._basePath
    }

    set basePath ( value ) {

        if ( isNull( value ) ) { throw new TypeError( 'Base path cannot be null ! Expect a non empty string.' ) }
        if ( isUndefined( value ) ) { throw new TypeError( 'Base path cannot be undefined ! Expect a non empty string.' ) }
        if ( isNotString( value ) ) { throw new TypeError( `Base path cannot be an instance of ${ value.constructor.name } ! Expect a non empty string.` ) }
        if ( isEmptyString( value ) ) { throw new TypeError( 'Base path cannot be empty ! Expect a non empty string.' ) }
        if ( isBlankString( value ) ) { throw new TypeError( 'Base path cannot contain only whitespace ! Expect a non empty string.' ) }

        this._basePath = value

    }

    /**
     *
     * @returns {*}
     */
    get responseType () {
        return this._responseType
    }

    set responseType ( value ) {

        if ( isNull( value ) ) { throw new Error( 'TDataBaseManager: responseType cannot be null !' ) }
        if ( isNull( value ) ) { throw new TypeError( 'Response type cannot be null ! Expect a non empty string.' ) }
        if ( isUndefined( value ) ) { throw new TypeError( 'Response type cannot be undefined ! Expect a non empty string.' ) }
        //        if ( !( value instanceof ResponseType ) ) { throw new TypeError( `Response type cannot be an instance of ${value.constructor.name} ! Expect a value from ResponseType enum.` ) }

        this._responseType = value

    }

    /**
     *
     * @returns {*}
     */
    get bunchSize () {
        return this._bunchSize
    }

    set bunchSize ( value ) {

        if ( isNull( value ) ) { throw new TypeError( 'Bunch size cannot be null ! Expect a positive number.' ) }
        if ( isUndefined( value ) ) { throw new TypeError( 'Bunch size cannot be undefined ! Expect a positive number.' ) }
        if ( isNotNumber( value ) ) { throw new TypeError( `Bunch size cannot be an instance of ${ value.constructor.name } ! Expect a positive number.` ) }
        if ( !isNumberPositive( value ) ) { throw new TypeError( `Bunch size cannot be lower or equal to zero ! Expect a positive number.` ) }

        this._bunchSize = value

    }

    /**
     *
     * @returns {*}
     */
    get requestAggregationTime () {
        return this._requestAggregationTime
    }

    set requestAggregationTime ( value ) {

        if ( isNull( value ) ) {
            throw new TypeError( 'Requests aggregation time cannot be null ! Expect a positive number.' )
        }

        if ( isUndefined( value ) ) {
            throw new TypeError( 'Requests aggregation time cannot be undefined ! Expect a positive number.' )
        }

        if ( isNotNumber( value ) ) {
            throw new TypeError( `Requests aggregation time cannot be an instance of ${ value.constructor.name } ! Expect a positive number.` )
        }

        if ( isNumberNegative( value ) ) {
            throw new TypeError( 'Requests aggregation time cannot be lower or equal to zero ! Expect a positive number.' )
        }

        this._requestAggregationTime = value

    }

    /**
     *
     * @returns {*}
     */
    get requestsConcurrency () {
        return this._requestsConcurrency
    }

    set requestsConcurrency ( value ) {

        if ( isNull( value ) ) {
            throw new TypeError( 'Minimum of simultaneous request cannot be null ! Expect a positive number.' )
        }

        if ( isUndefined( value ) ) {
            throw new TypeError( 'Minimum of simultaneous request cannot be undefined ! Expect a positive number.' )
        }

        if ( isNotNumber( value ) ) {
            throw new TypeError( `Minimum of simultaneous request cannot be an instance of ${ value.constructor.name } ! Expect a positive number.` )
        }

        if ( isZero( value ) || isNumberNegative( value ) ) {
            throw new TypeError( 'Minimum of simultaneous request cannot be lower or equal to zero ! Expect a positive number.' )
        }

        this._requestsConcurrency = value

    }

    /**
     *
     * @returns {TLogger}
     */
    get logger () {
        return this._logger
    }

    set logger ( value ) {

        if ( isNull( value ) ) { throw new TypeError( 'Progress manager cannot be null ! Expect an instance of TProgressManager.' ) }
        if ( isUndefined( value ) ) { throw new TypeError( 'Progress manager cannot be undefined ! Expect an instance of TProgressManager.' ) }
        if ( !( value instanceof TLogger ) ) { throw new TypeError( `Progress manager cannot be an instance of ${ value.constructor.name } ! Expect an instance of TProgressManager.` ) }

        this._logger = value

    }

    /**
     *
     * @param value
     * @returns {TDataBaseManager}
     */
    setBasePath ( value ) {

        this.basePath = value
        return this

    }

    /**
     *
     * @param value
     * @returns {TDataBaseManager}
     */
    setResponseType ( value ) {

        this.responseType = value
        return this

    }

    /**
     *
     * @param value
     * @returns {TDataBaseManager}
     */
    setBunchSize ( value ) {

        this.bunchSize = value
        return this

    }

    /**
     *
     * @param value
     * @returns {TDataBaseManager}
     */
    setRequestAggregationTime ( value ) {

        this.requestAggregationTime = value
        return this

    }

    /**
     *
     * @param value
     * @returns {TDataBaseManager}
     */
    setRequestsConcurrency ( value ) {

        this.requestsConcurrency = value
        return this

    }

    /**
     *
     * @param value
     * @returns {TDataBaseManager}
     */
    setLogger ( value ) {

        this.logger = value
        return this

    }

    /**
     *
     */
    aggregateQueue () {

        clearTimeout( this._aggregationTimeoutId )

        this._aggregationTimeoutId = setTimeout( () => {

            const datasToRequest = this._idToRequest
            let idBunch          = []
            for ( let idIndex = datasToRequest.length - 1 ; idIndex >= 0 ; idIndex-- ) {

                idBunch.push( datasToRequest.pop() )

                if ( idBunch.length === this._bunchSize || idIndex === 0 ) {

                    this._requestQueue.push( {
                        _id:        `readMany_${ Generate.id }`,
                        _timeStart: new Date(),
                        _type:      RequestType.ReadMany,
                        method:     HttpVerb.Read.value,
                        url:        this._basePath,
                        data:       {
                            ids: idBunch
                        },
                        responseType: this._responseType
                    } )

                    idBunch = []
                }

            }

            this.processQueue.call( this )

        }, this._requestAggregationTime )

    }

    /**
     *
     */
    processQueue () {

        while ( this._requestQueue.length > 0 && this._processQueue.length < this._requestsConcurrency ) {

            const requestSkull = this._requestQueue.pop()
            this._processQueue.push( requestSkull )

            const request              = new XMLHttpRequest()
            request.onloadstart        = _onLoadStart.bind( this )
            request.onload             = this._onLoad.bind( this,
                requestSkull,
                this._onEnd.bind( this, requestSkull, requestSkull.onLoad ),
                this._onProgress.bind( this, requestSkull.onProgress ),
                this._onError.bind( this, requestSkull, requestSkull.onError )
            )
            request.onloadend          = _onLoadEnd.bind( this )
            request.onprogress         = this._onProgress.bind( this, requestSkull.onProgress )
            request.onreadystatechange = _onReadyStateChange.bind( this )
            request.onabort            = _onAbort.bind( this )
            request.onerror            = this._onError.bind( this, requestSkull, requestSkull.onError )
            request.ontimeout          = _onTimeout.bind( this )
            request.open( requestSkull.method, requestSkull.url, true )
            request.setRequestHeader( 'Content-Type', 'application/json' )
            request.setRequestHeader( 'Accept', 'application/json' )
            request.responseType = requestSkull.responseType.value

            const dataToSend = ( requestSkull.data && requestSkull.responseType === ResponseType.Json ) ? JSON.stringify( requestSkull.data ) : requestSkull.data
            request.send( dataToSend )

        }

        function _onLoadStart ( loadStartEvent ) { this.logger.progress( loadStartEvent ) }

        function _onLoadEnd ( loadEndEvent ) { this.logger.progress( loadEndEvent ) }

        function _onReadyStateChange ( readyStateEvent ) { this.logger.debug( readyStateEvent ) }

        function _onAbort ( abortEvent ) { this.logger.error( abortEvent ) }

        function _onTimeout ( timeoutEvent ) { this.logger.error( timeoutEvent ) }

    }

    // Publics
    /**
     * The create method allow to create a new ressource on the server. Providing a single object that match a database schema, or an array of them.
     *
     * @param {object|array.<object>} data - The data to send for create new objects.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    create ( data, onLoadCallback, onProgressCallback, onErrorCallback ) {

        if ( isArray( data ) && isNotEmptyArray( data ) ) {

            if ( isArrayOfSingleElement( data ) ) {

                this._createOne( data[ 0 ], onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._createMany( data, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else if ( isObject( data ) && isNotEmptyObject( data ) ) {

            this._createOne( data, onLoadCallback, onProgressCallback, onErrorCallback )

        } else {

            onErrorCallback( 'TDataBaseManager.create: Invalid data type, expect object or array of objects.' )

        }

    }

    /**
     * The read method allow to retrieve data from the server, using a single id or an array of them.
     *
     * @param {string|array.<string>} condition - The ids of objects to retrieve.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    read ( condition, projection, onLoadCallback, onProgressCallback, onErrorCallback ) {

        if ( isString( condition ) && isNotEmptyString( condition ) && isNotBlankString( condition ) ) {

            this._readOne( condition, projection, onLoadCallback, onProgressCallback, onErrorCallback )

        } else if ( isArray( condition ) && isNotEmptyArray( condition ) ) {

            if ( isArrayOfSingleElement( condition ) ) {

                this._readOne( condition[ 0 ], projection, onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._readMany( condition, projection, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else if ( isObject( condition ) ) {

            if ( isEmptyObject( condition ) ) {

                this._readAll( projection, onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._readWhere( condition, projection, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else {

            onErrorCallback( 'TDataBaseManager.read: Invalid data type, expect string, object or array of objects.' )

        }

    }

    /**
     * The update method allow to update data on the server, using a single id or an array of them, and a corresponding object about the data to update.
     *
     * @param {string|array.<string>} condition - The ids of objects to update.
     * @param {object} update - The update data ( need to match the related database schema ! ). In case of multiple ids they will be updated with the same given data.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    update ( condition, update, onLoadCallback, onProgressCallback, onErrorCallback ) {

        if ( isNotDefined( update ) ) {
            onErrorCallback( 'TDataBaseManager.update: Update data cannot be null or undefined !' )
            return
        }

        if ( isNotObject( update ) ) {
            onErrorCallback( 'TDataBaseManager.update: Invalid update data type. Expect an object.' )
            return
        }

        if ( isString( condition ) && isNotEmptyString( condition ) && isNotBlankString( condition ) ) {

            this._updateOne( condition, update, onLoadCallback, onProgressCallback, onErrorCallback )

        } else if ( isArray( condition ) && isNotEmptyArray( condition ) ) {

            if ( isArrayOfSingleElement( condition ) ) {

                this._updateOne( condition[ 0 ], update, onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._updateMany( condition, update, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else if ( isObject( condition ) ) {

            if ( isEmptyObject( condition ) ) {

                this._updateAll( update, onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._updateWhere( condition, update, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else {

            onErrorCallback( 'TDataBaseManager.update: Invalid data type, expect string, object or array of objects.' )

        }

    }

    /**
     * The delete method allow to remove data from the server, using a single id or an array of them.
     *
     * @param {string|array.<string>|object|null} condition - The ids of objects to delete.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    delete ( condition, onLoadCallback, onProgressCallback, onErrorCallback ) {

        if ( isString( condition ) && isNotEmptyString( condition ) && isNotBlankString( condition ) ) {

            this._deleteOne( condition, onLoadCallback, onProgressCallback, onErrorCallback )

        } else if ( isArray( condition ) && isNotEmptyArray( condition ) ) {

            if ( isArrayOfSingleElement( condition ) ) {

                this._deleteOne( condition[ 0 ], onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._deleteMany( condition, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else if ( isObject( condition ) ) {

            if ( isEmptyObject( condition ) ) {

                this._deleteAll( onLoadCallback, onProgressCallback, onErrorCallback )

            } else {

                this._deleteWhere( condition, onLoadCallback, onProgressCallback, onErrorCallback )

            }

        } else {

            onErrorCallback( 'TDataBaseManager.delete: Invalid data type, expect null, string, object or array of objects.' )

        }

    }

    // Privates

    //// Events

    /**
     * The private _onLoad method allow to process the server response in an abstract way to check against error and wrong status code.
     * It will bind user callback on each type of returns, and dispatch in sub methods in function of the response type.
     *
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     * @param {object} loadEvent - The server response object to parse.
     * @private
     */
    _onLoad ( request, onLoadCallback, onProgressCallback, onErrorCallback, loadEvent ) {

        const target       = loadEvent.target
        const status       = target.status
        const response     = target.response
        const responseType = target.responseType

        switch ( status ) {

            // 100
            //            case HttpStatusCode.Continue.value:
            //            case HttpStatusCode.SwitchingProtocols.value:
            //            case HttpStatusCode.Processing.value:

            // 200
            case HttpStatusCode.Ok.value:
                this._dispatchResponse( response, responseType, onLoadCallback, onProgressCallback, onErrorCallback )
                break
            //            case HttpStatusCode.Created.value:
            //            case HttpStatusCode.Accepted.value:

            case HttpStatusCode.NonAuthoritativeInformation.value:
            case HttpStatusCode.NoContent.value:
            case HttpStatusCode.ResetContent.value:
            case HttpStatusCode.PartialContent.value:
            case HttpStatusCode.MultiStatus.value:
            case HttpStatusCode.AlreadyReported.value:
            case HttpStatusCode.ContentDifferent.value:
            case HttpStatusCode.IMUsed.value:
            case HttpStatusCode.MultipleChoices.value:
            case HttpStatusCode.MovedPermanently.value:
            case HttpStatusCode.Found.value:
            case HttpStatusCode.SeeOther.value:
            case HttpStatusCode.NotModified.value:
            case HttpStatusCode.UseProxy.value:
            case HttpStatusCode.Unused.value:
            case HttpStatusCode.TemporaryRedirect.value:
            case HttpStatusCode.PermanentRedirect.value:
            case HttpStatusCode.TooManyRedirects.value:
            case HttpStatusCode.BadRequest.value:
            case HttpStatusCode.Unauthorized.value:
            case HttpStatusCode.PaymentRequired.value:
            case HttpStatusCode.Forbidden.value:
            case HttpStatusCode.NotFound.value:
            case HttpStatusCode.MethodNotAllowed.value:
            case HttpStatusCode.NotAcceptable.value:
            case HttpStatusCode.ProxyAuthenticationRequired.value:
            case HttpStatusCode.RequestTimeOut.value:
            case HttpStatusCode.Conflict.value:
            case HttpStatusCode.Gone.value:
            case HttpStatusCode.LengthRequired.value:
            case HttpStatusCode.PreconditionFailed.value:
            case HttpStatusCode.RequestEntityTooLarge.value:
            case HttpStatusCode.RequestRangeUnsatisfiable.value:
            case HttpStatusCode.ExpectationFailed.value:
            case HttpStatusCode.ImATeapot.value:
            case HttpStatusCode.BadMapping.value:
            case HttpStatusCode.UnprocessableEntity.value:
            case HttpStatusCode.Locked.value:
            case HttpStatusCode.MethodFailure.value:
            case HttpStatusCode.UnorderedCollection.value:
            case HttpStatusCode.UpgradeRequired.value:
            case HttpStatusCode.PreconditionRequired.value:
            case HttpStatusCode.TooManyRequests.value:
            case HttpStatusCode.RequestHeaderFieldsTooLarge.value:
            case HttpStatusCode.NoResponse.value:
            case HttpStatusCode.RetryWith.value:
            case HttpStatusCode.BlockedByWindowsParentalControls.value:
            case HttpStatusCode.UnavailableForLegalReasons.value:
            case HttpStatusCode.UnrecoverableError.value:
            case HttpStatusCode.SSLCertificateError.value:
            case HttpStatusCode.SSLCertificateRequired.value:
            case HttpStatusCode.HTTPRequestSentToHTTPSPort.value:
            case HttpStatusCode.ClientClosedRequest.value:
            case HttpStatusCode.InternalServerError.value:
            case HttpStatusCode.NotImplemented.value:
            case HttpStatusCode.BadGateway.value:
            case HttpStatusCode.ServiceUnavailable.value:
            case HttpStatusCode.GatewayTimeOut.value:
            case HttpStatusCode.HTTPVersionNotSupported.value:
            case HttpStatusCode.VariantAlsoNegotiates.value:
            case HttpStatusCode.InsufficientStorage.value:
            case HttpStatusCode.LoopDetected.value:
            case HttpStatusCode.BandwidthLimitExceeded.value:
            case HttpStatusCode.NotExtended.value:
            case HttpStatusCode.NetworkAuthenticationRequired.value:
            case HttpStatusCode.UnknownError.value:
            case HttpStatusCode.WebServerIsDown.value:
            case HttpStatusCode.ConnectionTimedOut.value:
            case HttpStatusCode.OriginIsUnreachable.value:
            case HttpStatusCode.ATimeoutOccured.value:
            case HttpStatusCode.SSLHandshakeFailed.value:
            case HttpStatusCode.InvalidSSLCertificate.value:
            case HttpStatusCode.RailgunError.value:
                onErrorCallback( response )
                break

            default:
                throw new RangeError( `Unmanaged HttpStatusCode: ${ status }` )

        }

    }

    /**
     * The private _onProgress method will handle all progress event from server and submit them to the logger if exist else to the user onProgressCallback
     *
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {object} progressEvent - The server progress event.
     * @private
     */
    _onProgress ( onProgressCallback, progressEvent ) {

        if ( isDefined( this.logger ) ) {

            this.logger.progress( progressEvent, onProgressCallback )

        } else if ( isDefined( onProgressCallback ) ) {

            onProgressCallback( progressEvent )

        }

    }

    /**
     * The private _onError method will handle all error event from server and submit them to the logger if exist else to the user onErrorCallback
     *
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     * @param {object} errorEvent - A server error event
     * @private
     */
    _onError ( request, onErrorCallback, errorEvent ) {

        this._closeRequest( request )

        if ( isDefined( this.logger ) ) {

            this.logger.error( errorEvent, onErrorCallback )

        } else if ( isDefined( onErrorCallback ) ) {

            onErrorCallback( errorEvent )

        }

    }

    /**
     * The private _onEnd method is call after all other callback and perform request type checking in view to upadte cache, waitingqueue and callback if needed,
     * to finally close the request
     *
     * @param request
     * @param onLoadCallback
     * @param response
     * @private
     */
    _onEnd ( request, onLoadCallback, response ) {

        const type = request._type

        switch ( type ) {

            case RequestType.ReadOne:
            case RequestType.ReadMany:
                this._updateCache( response )
                this._updateWaitingQueue()
                break

            case RequestType.ReadWhere:
            case RequestType.ReadAll:
                this._updateCache( response )
                this._updateWaitingQueue()
                onLoadCallback( response )
                break

            case RequestType.CreateOne:
            case RequestType.CreateMany:
            case RequestType.UpdateOne:
            case RequestType.UpdateMany:
            case RequestType.UpdateWhere:
            case RequestType.UpdateAll:
            case RequestType.DeleteOne:
            case RequestType.DeleteMany:
            case RequestType.DeleteWhere:
            case RequestType.DeleteAll:
                onLoadCallback( response )
                break

            default:
                throw new RangeError( `Invalid request type: ${ type }` )

        }

        this._closeRequest( request )

    }

    //// Data parsing
    // Expect that methods were reimplemented when TDataBaseManager is inherited

    /**
     * Dispatch response to the correct handler in function of response type
     *
     * @param response
     * @param responseType
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _dispatchResponse ( response, responseType, onLoadCallback, onProgressCallback, onErrorCallback ) {

        switch ( responseType ) {

            case ResponseType.ArrayBuffer.value:
                this._onArrayBuffer(
                    response,
                    onLoadCallback,
                    onProgressCallback,
                    onErrorCallback
                )
                break

            case ResponseType.Blob.value:
                this._onBlob(
                    response,
                    onLoadCallback,
                    onProgressCallback,
                    onErrorCallback
                )
                break

            case ResponseType.Json.value:
                this._onJson(
                    response,
                    onLoadCallback,
                    onProgressCallback,
                    onErrorCallback
                )
                break

            case ResponseType.DOMString.value:
            case ResponseType.Default.value:
                this._onText(
                    response,
                    onLoadCallback,
                    onProgressCallback,
                    onErrorCallback
                )
                break

            default:
                throw new Error( `Unknown response type: ${ responseType }` )

        }

    }

    /**
     * Will remove the request from the process queue
     *
     * @param request
     * @private
     */
    _closeRequest ( request ) {

        this._processQueue.splice( this._processQueue.indexOf( request ), 1 )

        if ( Window.Itee && Window.Itee.Debug ) {

            const diff    = new Date().valueOf() - request._timeStart.valueOf()
            const message = `${ this.constructor.name } close request [${ request._id }] on ${ diff }ms.` +
                `Waiting queue: ${ this._waitingQueue.length }` +
                `Request queue: ${ this._requestQueue.length }` +
                `Process queue: ${ this._processQueue.length }` +
                `==========================`
            this.logger.debug( message )

        }

        this.processQueue()

    }

    /**
     *
     * @param ids
     * @returns {Object}
     * @private
     */
    _retrieveCachedValues ( ids ) {

        let results      = {}
        let underRequest = []
        let toRequest    = []

        for ( let idIndex = 0, numberOfIds = ids.length ; idIndex < numberOfIds ; idIndex++ ) {

            const id          = ids[ idIndex ]
            const cachedValue = this._cache.get( id )

            if ( isDefined( cachedValue ) ) {
                results[ id ] = cachedValue
            } else if ( isNull( cachedValue ) ) { // In request
                underRequest.push( id )
            } else {
                toRequest.push( id )
            }

        }

        return {
            results,
            underRequest,
            toRequest
        }

    }

    /**
     *
     * @param datas
     * @private
     */
    _updateCache ( datas ) {

        if ( isNull( datas ) ) { throw new TypeError( 'Data cannot be null ! Expect an array of object.' ) }
        if ( isUndefined( datas ) ) { throw new TypeError( 'Data cannot be undefined ! Expect an array of object.' ) }

        let _datas = {}
        if ( isArray( datas ) ) {

            for ( let key in datas ) {
                _datas[ datas[ key ]._id ] = datas[ key ]
            }

        } else {

            _datas = datas

        }

        for ( let [ id, data ] of Object.entries( _datas ) ) {

            const cachedResult = this._cache.get( id )

            if ( isNull( cachedResult ) ) {
                this._cache.add( id, data, true )
            } else if ( isUndefined( cachedResult ) ) {
                this.logger.warn( 'Cache was not pre-allocated with null value.' )
                this._cache.add( id, data )
            } else {
                this.logger.error( 'Cached value already exist !' )
            }

        }

    }

    /**
     *
     * @private
     */
    _updateWaitingQueue () {

        const haveNoRequestToProcess = ( this._requestQueue.length === 0 && this._processQueue.length === 0 )

        for ( let requestIndex = this._waitingQueue.length - 1 ; requestIndex >= 0 ; requestIndex-- ) {

            const demand = this._waitingQueue[ requestIndex ]

            // Update requested datas
            for ( let dataIndex = demand.underRequest.length - 1 ; dataIndex >= 0 ; dataIndex-- ) {

                const id           = demand.underRequest[ dataIndex ]
                const cachedResult = this._cache.get( id )

                if ( isNotDefined( cachedResult ) ) { continue }

                // Assign the cached value
                demand.results[ id ] = cachedResult

                // Remove the requested object that is now added
                demand.underRequest.splice( demand.underRequest.indexOf( id ), 1 )

            }

            // Check if request is now fullfilled
            const demandIsComplet = ( demand.underRequest.length === 0 )
            if ( demandIsComplet ) {

                this._waitingQueue.splice( requestIndex, 1 )
                demand.onLoadCallback( demand.results )

            } else if ( !demandIsComplet && haveNoRequestToProcess /* && haveTryAgainManyTimesButFail */ ) {

                this.logger.warn( 'Incomplet demand but empty request/process queue' )
                this._waitingQueue.splice( requestIndex, 1 )
                demand.onLoadCallback( demand.results )

            } else {

                // Wait next response

            }

        }

    }

    /**
     * The abstract private _onArrayBuffer method must be overridden in case the parser expect an array buffer as input data.
     *
     * @private
     * @abstract
     * @param {ArrayBuffer} data - The retrieved data to parse.
     * @param {function} onSuccess - The onLoad callback, which is call when parser parse with success the data.
     * @param {function} onProgress - The onProgress callback, which is call during the parsing.
     * @param {function} onError - The onError callback, which is call when parser throw an error during parsing.
     */
    // eslint-disable-next-line no-unused-vars
    _onArrayBuffer ( data, onSuccess, onProgress, onError ) {}

    /**
     * The abstract private _onBlob method must be overridden in case the parser expect a blob as input data.
     *
     * @private
     * @abstract
     * @param {Blob} data - The retrieved data to parse.
     * @param {function} onSuccess - The onLoad callback, which is call when parser parse with success the data.
     * @param {function} onProgress - The onProgress callback, which is call during the parsing.
     * @param {function} onError - The onError callback, which is call when parser throw an error during parsing.
     */
    // eslint-disable-next-line no-unused-vars
    _onBlob ( data, onSuccess, onProgress, onError ) {}

    /**
     * The abstract private _onJson method must be overridden in case the parser expect json as input data.
     *
     * @private
     * @abstract
     * @param {json} data - The retrieved data to parse.
     * @param {function} onSuccess - The onLoad callback, which is call when parser parse with success the data.
     * @param {function} onProgress - The onProgress callback, which is call during the parsing.
     * @param {function} onError - The onError callback, which is call when parser throw an error during parsing.
     */
    // eslint-disable-next-line no-unused-vars
    _onJson ( data, onSuccess, onProgress, onError ) {}

    /**
     * The abstract private _onText method must be overridden in case the parser expect a string/text as input data.
     *
     * @private
     * @abstract
     * @param {string} data - The retrieved data to parse.
     * @param {function} onSuccess - The onLoad callback, which is call when parser parse with success the data.
     * @param {function} onProgress - The onProgress callback, which is call during the parsing.
     * @param {function} onError - The onError callback, which is call when parser throw an error during parsing.
     */
    // eslint-disable-next-line no-unused-vars
    _onText ( data, onSuccess, onProgress, onError ) {}

    // REST Api calls
    /**
     * The private _create method allow to format a server request to create objects with the given data and get creation result with given callbacks.
     *
     * @private
     * @param {object} data - The data to send.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    _createOne ( data, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:          `createOne_${ Generate.id }`,
            _timeStart:   new Date(),
            _type:        RequestType.CreateOne,
            method:       HttpVerb.Create.value,
            url:          this._basePath,
            data:         data,
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     *
     * @param datas
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _createMany ( datas, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:          `createMany_${ Generate.id }`,
            _timeStart:   new Date(),
            _type:        RequestType.CreateMany,
            method:       HttpVerb.Create.value,
            url:          this._basePath,
            data:         datas,
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     * The private _updateOne method will format a server request to get a single object with the given id.
     *
     * @private
     * @param {string} id - The object's id of the object to retrieve.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    _readOne ( id, projection, onLoadCallback, onProgressCallback, onErrorCallback ) {

        // Filter requested values by cached values
        const datas = this._retrieveCachedValues( [ id ] )

        // retrieveLocalStorageValues...

        // getDatabaseValues()

        if ( datas.toRequest.length === 0 ) {

            if ( datas.underRequest.length === 0 ) {

                onLoadCallback( datas.results )

            } else {

                datas[ 'onLoadCallback' ]     = onLoadCallback
                datas[ 'onProgressCallback' ] = onProgressCallback
                datas[ 'onErrorCallback' ]    = onErrorCallback
                this._waitingQueue.push( datas )

            }

        } else {

            datas[ 'onLoadCallback' ]     = onLoadCallback
            datas[ 'onProgressCallback' ] = onProgressCallback
            datas[ 'onErrorCallback' ]    = onErrorCallback
            this._waitingQueue.push( datas )

            try {
                this._cache.add( id, null )
                datas.underRequest.push( id )
                datas.toRequest.splice( datas.toRequest.indexOf( id ), 1 )
            } catch ( error ) {
                this.logger.error( error )
            }

            this._idToRequest.push( id )
            this.aggregateQueue()

        }

    }

    /**
     * The private _readMany method will format a server request to get objects with id in the ids array.
     *
     * @private
     * @param {array.<string>} ids - The ids of objects to retrieve.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     */
    _readMany ( ids, projection, onLoadCallback, onProgressCallback, onErrorCallback ) {

        // Filter requested values by cached values
        const datas = this._retrieveCachedValues( ids )

        // retrieveLocalStorageValues...

        // getDatabaseValues()

        if ( datas.toRequest.length === 0 ) {

            if ( datas.underRequest.length === 0 ) {

                onLoadCallback( datas.results )

            } else {

                datas[ 'onLoadCallback' ]     = onLoadCallback
                datas[ 'onProgressCallback' ] = onProgressCallback
                datas[ 'onErrorCallback' ]    = onErrorCallback
                this._waitingQueue.push( datas )

            }

        } else {

            datas[ 'onLoadCallback' ]     = onLoadCallback
            datas[ 'onProgressCallback' ] = onProgressCallback
            datas[ 'onErrorCallback' ]    = onErrorCallback
            this._waitingQueue.push( datas )

            const datasToRequest = datas.toRequest
            let id               = undefined
            for ( let idIndex = datasToRequest.length - 1 ; idIndex >= 0 ; idIndex-- ) {

                id = datasToRequest[ idIndex ]

                // Prepare entry for id to request
                try {
                    this._cache.add( id, null )
                    datas.underRequest.push( id )
                    datas.toRequest.splice( datas.toRequest.indexOf( id ), 1 )
                } catch ( error ) {
                    this.logger.error( error )
                }

                this._idToRequest.push( id )

            }

            this.aggregateQueue()

        }

    }

    /**
     *
     * @param query
     * @param projection
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _readWhere ( query, projection, onLoadCallback, onProgressCallback, onErrorCallback ) {

        //        // Filter requested values by cached values
        //                const datas = {
        //                    results: {},
        //                    underRequest: [],
        //                    toRequest: []
        //                }
        //
        //        datas[ 'onLoadCallback' ] = onLoadCallback
        //        this._waitingQueue.push( datas )

        this._requestQueue.push( {
            _id:        `readWhere_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.ReadWhere,
            method:     HttpVerb.Read.value,
            url:        this._basePath,
            data:       {
                query,
                projection
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     *
     * @param projection
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _readAll ( projection, onLoadCallback, onProgressCallback, onErrorCallback ) {

        //        const datas = {
        //            results: {},
        //            underRequest: [],
        //            toRequest: []
        //        }
        //
        //        datas[ 'onLoadCallback' ] = onLoadCallback
        //        this._waitingQueue.push( datas )

        const query = {}

        this._requestQueue.push( {
            _id:        `readAll_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.ReadAll,
            method:     HttpVerb.Read.value,
            url:        this._basePath,
            data:       {
                query,
                projection
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     * The private _updateOne method will format a server request to update a single object with the given id.
     *
     * @param {string} id - The object's id of the object to update.
     * @param update
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     * @private
     */
    _updateOne ( id, update, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:        `updateOne_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.UpdateOne,
            method:     HttpVerb.Update.value,
            url:        `${ this._basePath }/${ id }`,
            data:       {
                update
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     * The private _updateMany method will format a server request to update objects with id in the ids array.
     *
     * @param {array.<string>} ids - The ids of objects to update.
     * @param update
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     * @private
     */
    _updateMany ( ids, update, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:        `updateMany_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.UpdateMany,
            method:     HttpVerb.Update.value,
            url:        this._basePath,
            data:       {
                ids,
                update
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     *
     * @param query
     * @param update
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _updateWhere ( query, update, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:        `updateWhere_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.UpdateWhere,
            method:     HttpVerb.Update.value,
            url:        this._basePath,
            data:       {
                query,
                update
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     *
     * @param update
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _updateAll ( update, onLoadCallback, onProgressCallback, onErrorCallback ) {

        const query = {}

        this._requestQueue.push( {
            _id:        `updateAll_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.UpdateAll,
            method:     HttpVerb.Update.value,
            url:        this._basePath,
            data:       {
                query,
                update
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     * The private _deleteOne method will format a server request to delete a single object with the given id.
     *
     * @param {string} id - The object's id of the object to delete.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     * @private
     */
    _deleteOne ( id, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:          `deleteOne_${ Generate.id }`,
            _timeStart:   new Date(),
            _type:        RequestType.DeleteOne,
            method:       HttpVerb.Delete.value,
            url:          `${ this._basePath }/${ id }`,
            data:         null,
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     * The private _deleteMany method will format a server request to delete objects with id in the ids array.
     *
     * @param {array.<string>} ids - The ids of objects to delete.
     * @param {function} onLoadCallback - The onLoad callback, which is call when server respond with success to the request.
     * @param {function} onProgressCallback - The onProgress callback, which is call during the response incoming.
     * @param {function} onErrorCallback - The onError callback, which is call when server respond with an error to the request.
     * @private
     */
    _deleteMany ( ids, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:        `deleteMany_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.DeleteMany,
            method:     HttpVerb.Delete.value,
            url:        this._basePath,
            data:       {
                ids
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     *
     * @param query
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _deleteWhere ( query, onLoadCallback, onProgressCallback, onErrorCallback ) {

        this._requestQueue.push( {
            _id:        `deleteWhere_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.DeleteWhere,
            method:     HttpVerb.Delete.value,
            url:        this._basePath,
            data:       {
                query
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

    /**
     *
     * @param onLoadCallback
     * @param onProgressCallback
     * @param onErrorCallback
     * @private
     */
    _deleteAll ( onLoadCallback, onProgressCallback, onErrorCallback ) {

        const query = {}

        this._requestQueue.push( {
            _id:        `deleteAll_${ Generate.id }`,
            _timeStart: new Date(),
            _type:      RequestType.DeleteAll,
            method:     HttpVerb.Delete.value,
            url:        this._basePath,
            data:       {
                query
            },
            onLoad:       onLoadCallback,
            onProgress:   onProgressCallback,
            onError:      onErrorCallback,
            responseType: this._responseType
        } )

        this.processQueue()

    }

}

// Static stuff
/**
 *
 * @type {number}
 * @private
 */
TDataBaseManager._requestId = 0

/**
 *
 * @type {Object}
 * @private
 */
TDataBaseManager._requests = {
    /**
     * The global waiting queue to process
     */
    waitingQueue: {},
    /**
     * The objects not requested yet
     */
    toProcess:    {
        create: {},
        read:   {},
        update: {},
        delete: {}
    },
    /**
     * The object currently under request
     */
    underProcess: {
        create: {},
        read:   {},
        update: {},
        delete: {}
    },
    /**
     * The objects already processed
     */
    processed: {
        create: {},
        read:   {},
        update: {},
        delete: {}
    }
}
//TDataBaseManager._orchestrator = TOrchestrator

export { TDataBaseManager }