import { Config , BASE_URL , SDK_VERISON , KV , protocol } from '../types' ; import { IRequestOptions , SDKRequestInterface , ResponseObject , IUploadRequestOptions , IRequestConfig } from '@cloudbase/adapter-interface' ; import { ICache , getCache , getLocalCache } from './cache' ; import { activateEvent , EVENTS } from './events' ; import { genSeqId , isFormData , formatUrl , createSign } from './util' ; import { Adapter , RUNTIME } from '../adapters' ; import { LOGINTYPE } from '../auth/base' ; interface GetAccessTokenResult { accessToken : string ; accessTokenExpire : number ; } export type CommonRequestOptions = { headers ? : KV < string > ; responseType ? : string ; onUploadProgress ? : Function ; } ; type AppendedRequestInfo = { data : KV < any > ; headers : KV < string > ; } ; interface RequestBeforeHook { ( ... args ) : AppendedRequestInfo ; } const actionsWithoutAccessToken = [ 'auth.getJwt' , 'auth.logout' , 'auth.signInWithTicket' , 'auth.signInAnonymously' , 'auth.signIn' , 'auth.fetchAccessTokenWithRefreshToken' , 'auth.signUpWithEmailAndPassword' , 'auth.activateEndUserMail' , 'auth.sendPasswordResetEmail' , 'auth.resetPasswordWithToken' , 'auth.isUsernameRegistered' ] ; const commonHeader = { 'X-SDK-Version' : `tcb-js-sdk/${ SDK_VERISON }` } ; function bindHooks ( instance , name , hooks ) { const originMethod = instance [ name ] ; instance [ name ] = function ( options ) { const data = { } ; const headers = { } ; hooks . forEach ( hook => { const { data : appendedData , headers : appendedHeaders } = hook . call ( instance , options ) ; Object . assign ( data , appendedData ) ; Object . assign ( headers , appendedHeaders ) ; } ) ; const originData = options . data ; originData && ( ( ) => { if ( isFormData ( originData ) ) { for ( const key in data ) { ( originData as FormData ) . append ( key , data [ key ] ) ; } return ; } options . data = { ... originData , ... data } ; } ) ( ) ; options . headers = { ... ( options . headers || { } ) , ... headers } ; return ( originMethod as Function ) . call ( instance , options ) ; } ; } function beforeEach ( ) { const seqId = genSeqId ( ) ; return { data : { seqId } , headers : { ... commonHeader , 'x-seqid' : seqId } } ; } class class IRequest { config : Config ; _shouldRefreshAccessTokenHook : Function ; _refreshAccessTokenPromise : Promise < GetAccessTokenResult > | null ; _reqClass : SDKRequestInterface ; private _cache : ICache ; private _localCache : ICache ; param config constructor ( config = { } ) { this . config = config ; this . _reqClass = new Adapter . adapter . reqClass ( < IRequestConfig > { timeout : this . config . timeout , timeoutMsg : `[tcb-js-sdk] 请求在${ this . config . timeout / 1000 }s内未完成，已中断` , restrictedMethods : [ 'post' ] } ) ; this . _cache = getCache ( this . config . env ) ; this . _localCache = getLocalCache ( this . config . env ) ; bindHooks ( this . _reqClass , 'post' , [ beforeEach ] ) ; bindHooks ( this . _reqClass , 'upload' , [ beforeEach ] ) ; bindHooks ( this . _reqClass , 'download' , [ beforeEach ] ) ; } async post ( options ) { const res = await this . _reqClass . post ( options ) ; return res ; } async upload ( options ) { const res = await this . _reqClass . upload ( options ) ; return res ; } async download ( options ) { const res = await this . _reqClass . download ( options ) ; return res ; } async refreshAccessToken ( ) { if ( ! this . _refreshAccessTokenPromise ) { this . _refreshAccessTokenPromise = this . _refreshAccessToken ( ) ; } let result ; let err ; try { result = await this . _refreshAccessTokenPromise ; } catch ( e ) { err = e ; } this . _refreshAccessTokenPromise = null ; this . _shouldRefreshAccessTokenHook = null ; if ( err ) { throw err ; } return result ; } async _refreshAccessToken ( retryNum = 1 ) { const { accessTokenKey , accessTokenExpireKey , refreshTokenKey , loginTypeKey , anonymousUuidKey } = this . _cache . keys ; this . _cache . removeStore ( accessTokenKey ) ; this . _cache . removeStore ( accessTokenExpireKey ) ; let refreshToken = this . _cache . getStore ( refreshTokenKey ) ; if ( ! refreshToken ) { throw new Error ( '[tcb-js-sdk] 未登录CloudBase' ) ; } const params = { refresh_token : refreshToken , } ; const response = await this . request ( 'auth.fetchAccessTokenWithRefreshToken' , params ) ; if ( response . data . code ) { const { code } = response . data ; if ( code === 'SIGN_PARAM_INVALID' || code === 'REFRESH_TOKEN_EXPIRED' || code === 'INVALID_REFRESH_TOKEN' ) { const isAnonymous = this . _cache . getStore ( loginTypeKey ) === LOGINTYPE . ANONYMOUS ; if ( isAnonymous && code === 'INVALID_REFRESH_TOKEN' ) { const anonymous_uuid = this . _cache . getStore ( anonymousUuidKey ) ; const refresh_token = this . _cache . getStore ( refreshTokenKey ) ; const res = await this . send ( 'auth.signInAnonymously' , { anonymous_uuid , refresh_token } ) ; this . setRefreshToken ( res . refresh_token ) ; if ( retryNum >= 1 ) { return this . _refreshAccessToken ( -- retryNum ) ; } else { throw new Error ( `[tcb-js-sdk] 重试获取 refresh token 失败` ) ; } } activateEvent ( EVENTS . LOGIN_STATE_EXPIRED ) ; this . _cache . removeStore ( refreshTokenKey ) ; } throw new Error ( `[tcb-js-sdk] 刷新access token失败：${ response . data . code }` ) ; } if ( response . data . access_token ) { activateEvent ( EVENTS . ACCESS_TOKEN_REFRESHD ) ; this . _cache . setStore ( accessTokenKey , response . data . access_token ) ; this . _cache . setStore ( accessTokenExpireKey , response . data . access_token_expire + Date . now ( ) ) ; return { accessToken : response . data . access_token , accessTokenExpire : response . data . access_token_expire } ; } if ( response . data . refresh_token ) { this . _cache . removeStore ( refreshTokenKey ) ; this . _cache . setStore ( refreshTokenKey , response . data . refresh_token ) ; this . _refreshAccessToken ( ) ; } } async getAccessToken ( ) { const { accessTokenKey , accessTokenExpireKey , refreshTokenKey } = this . _cache . keys ; const refreshToken = this . _cache . getStore ( refreshTokenKey ) ; if ( ! refreshToken ) { throw new Error ( '[tcb-js-sdk] refresh token不存在，登录状态异常' ) ; } let accessToken = this . _cache . getStore ( accessTokenKey ) ; let accessTokenExpire = this . _cache . getStore ( accessTokenExpireKey ) ; let shouldRefreshAccessToken = true ; if ( this . _shouldRefreshAccessTokenHook && ! ( await this . _shouldRefreshAccessTokenHook ( accessToken , accessTokenExpire ) ) ) { shouldRefreshAccessToken = false ; } if ( ( ! accessToken || ! accessTokenExpire || accessTokenExpire < Date . now ( ) ) && shouldRefreshAccessToken ) { return this . refreshAccessToken ( ) ; } else { return { accessToken , accessTokenExpire } ; } } async request ( action , params , options ? ) { const tcbTraceKey = `x-tcb-trace_${ this . config . env }` ; let contentType = 'application/x-www-form-urlencoded' ; const tmpObj = { action , env : this . config . env , ... params } ; if ( actionsWithoutAccessToken . indexOf ( action ) === - 1 ) { const { refreshTokenKey } = this . _cache . keys ; let refreshToken = this . _cache . getStore ( refreshTokenKey ) ; if ( refreshToken ) { tmpObj . access_token = ( await this . getAccessToken ( ) ) . accessToken ; } } let payload ; if ( action === 'storage.uploadFile' ) { payload = new FormData ( ) ; for ( let key in payload ) { if ( payload . hasOwnProperty ( key ) && payload [ key ] !== undefined ) { payload . append ( key , tmpObj [ key ] ) ; } } contentType = 'multipart/form-data' ; } else { contentType = 'application/json;charset=UTF-8' ; payload = { } ; for ( let key in tmpObj ) { if ( tmpObj [ key ] !== undefined ) { payload [ key ] = tmpObj [ key ] ; } } } let opts = { headers : { 'content-type' : contentType } } ; if ( options && options [ 'onUploadProgress' ] ) { opts . onUploadProgress = options [ 'onUploadProgress' ] ; } const traceHeader = this . _localCache . getStore ( tcbTraceKey ) ; if ( traceHeader ) { opts . headers [ 'X-TCB-Trace' ] = traceHeader ; } if ( Adapter . runtime !== RUNTIME . WEB ) { const { appSign , appSecret } = this . config ; const timestamp = Date . now ( ) ; const { appAccessKey , appAccessKeyId } = appSecret ; const sign = createSign ( { data : payload , timestamp , appAccessKeyId , appSign } , appAccessKey ) ; opts . headers [ 'X-TCB-App-Source' ] = `timestamp=${ timestamp };appAccessKeyId=${ appAccessKeyId };appSign=${ appSign };sign=${ sign }` ; } const { parse , inQuery , search } = params ; let formatQuery = { env : this . config . env } ; parse && ( formatQuery . parse = true ) ; inQuery && ( formatQuery = { ... inQuery , ... formatQuery } ) ; const hostname = BASE_URL . slice ( 2 ) ; const transformEnv = `//${ this . config . env }.${ hostname }` ; let newUrl = formatUrl ( protocol , transformEnv , formatQuery ) ; if ( search ) { newUrl += search ; } const res = await this . post ( { url : newUrl , data : payload , ... opts } ) ; const resTraceHeader = res . header && res . header [ 'x-tcb-trace' ] ; if ( resTraceHeader ) { this . _localCache . setStore ( tcbTraceKey , resTraceHeader ) ; } if ( ( Number ( res . status ) !== 200 && Number ( res . statusCode ) !== 200 ) || ! res . data ) { throw new Error ( 'network request error' ) ; } return res ; } async send ( action , data = { } ) { const response = await this . request ( action , data , { onUploadProgress : data . onUploadProgress } ) ; if ( response . data . code === 'ACCESS_TOKEN_EXPIRED' && actionsWithoutAccessToken . indexOf ( action ) === - 1 ) { await this . refreshAccessToken ( ) ; const response = await this . request ( action , data , { onUploadProgress : data . onUploadProgress } ) ; if ( response . data . code ) { throw new Error ( `[${ response . data . code }] ${ response . data . message }` ) ; } return response . data ; } if ( response . data . code ) { throw new Error ( `[${ response . data . code }] ${ response . data . message }` ) ; } return response . data ; } private setRefreshToken ( refreshToken ) { const { accessTokenKey , accessTokenExpireKey , refreshTokenKey } = this . _cache . keys ; this . _cache . removeStore ( accessTokenKey ) ; this . _cache . removeStore ( accessTokenExpireKey ) ; this . _cache . setStore ( refreshTokenKey , refreshToken ) ; } } const requestMap = { } ; function initRequest ( config ) { requestMap [ config . env ] = new IRequest ( config ) ; } function getRequestByEnvId ( env ) { return requestMap [ env ] ; } export { getRequestByEnvId , IRequest , initRequest } ;