/**
* @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 = /*#__PURE__*/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 }