import ApiError from '../shared/ApiError' ; import { ApiContract , ApiMethodResponse , ApiMethodResponseSuccess } from '../shared/types' ; import queueMicrotask from './queueMicrotask' ; interface ApiItem { method : string ; params : Record < string , unknown > | Record < string , unknown > [ ] ; resolve ( data ) : void ; reject ( err ) : void ; } interface ApiOptions { url : string ; csrfToken ? : string ; batchMaxSize ? : number ; timeout ? : number ; headers ? : RequestInit [ 'headers' ] | ( ( ) => RequestInit [ 'headers' ] ) ; } const DEFAULT_API_OPTIONS = { batchMaxSize : 1 , csrfToken : '' , timeout : 30000 , headers : { } } ; const MAX_RETRIES = 2 ; class Api < TApiContract extends ApiContract > { private options : Required < ApiOptions > ; private queue : ApiItem [ ] = [ ] ; constructor ( options ) { this . options = { ... DEFAULT_API_OPTIONS , ... options } ; } exec < TMethod extends Extract < keyof TApiContract , string > > ( ... [ method , params = { } ] ) { return new Promise ( ( resolve , reject ) => { const { options , queue } = this ; if ( options . batchMaxSize > 1 ) { queue . push ( { method , params , resolve , reject } ) ; switch ( queue . length ) { case options . batchMaxSize : this . processQueue ( ) ; break ; case 1 : queueMicrotask ( this . processQueue ) ; break ; } } else { this . doRequest ( { reject , method , params , resolve : ( res ) => { this . handleMethodResponse ( res , resolve , reject ) ; } } ) ; } } ) ; } private processQueue = ( ) : void => { const { queue } = this ; if ( queue . length === 0 ) { return ; } this . queue = [ ] ; this . doRequest ( { method : 'batch' , params : queue . map ( ( { method , params } ) => ( { method , params } ) ) , resolve : ( { data } ) => { queue . forEach ( ( { resolve , reject } , i ) => { const item = data [ i ] ; if ( typeof item !== 'object' || item === null ) { reject ( 'Incompatible format, expected object with data or error field' ) ; } this . handleMethodResponse ( item , resolve , reject ) ; } ) ; } , reject : ( err ) => { queue . forEach ( ( { reject } ) => { reject ( err ) ; } ) ; } } ) ; } ; private doRequest ( { resolve , reject , method , params , retries = 0 } ) { const { url , csrfToken , timeout , headers } = this . options ; let timeoutCancellationToken = window . setTimeout ( ( ) => { timeoutCancellationToken = null ; reject ( new ApiError ( ApiError . TIMEOUT ) ) ; } , timeout ) ; fetch ( `${ url }/${ method }` , { method : 'POST' , credentials : 'same-origin' , headers : { ... typeof headers === 'function' ? headers ( ) : headers , ... csrfToken ? { 'X-Csrf-Token' : csrfToken } : undefined } , body : JSON . stringify ( params ) } ) . then ( response => { if ( timeoutCancellationToken === null ) { throw new ApiError ( ApiError . TIMEOUT ) ; } window . clearTimeout ( timeoutCancellationToken ) ; if ( response . ok ) { return response . json ( ) . catch ( err => { throw new ApiError ( ApiError . INTERNAL_ERROR , err . message , err ) ; } ) ; } if ( csrfToken ) { const newCsrfToken = response . headers . get ( 'X-Csrf-Token' ) ; if ( newCsrfToken && newCsrfToken !== csrfToken ) { this . options . csrfToken = newCsrfToken ; throw new ApiError ( ApiError . WRONG_CSRF_TOKEN ) ; } } throw new ApiError ( ApiError . INTERNAL_ERROR , response . statusText ) ; } , err => { if ( timeoutCancellationToken === null ) { throw new ApiError ( ApiError . TIMEOUT ) ; } window . clearTimeout ( timeoutCancellationToken ) ; throw new ApiError ( ApiError . INTERNAL_ERROR , err . message , err ) ; } ) . then ( ( res ) => { if ( typeof res === 'object' && res !== null ) { resolve ( res ) ; } else { throw new ApiError ( ApiError . INTERNAL_ERROR , 'Incompatible response format' ) ; } } ) . catch ( err => { if ( err && err . type === ApiError . WRONG_CSRF_TOKEN && retries < MAX_RETRIES ) { this . doRequest ( { resolve , reject , method , params , retries : retries + 1 } ) ; } else { reject ( err ) ; } } ) ; } private handleMethodResponse ( response , resolve , reject ) { if ( response . error ) { reject ( new ApiError ( response . error . type , response . error . message , response . error . data ) ) ; } else { resolve ( response . data ) ; } } } export default Api ; export { ApiOptions } ;