HTTPOperation.ts · GitHub
source link: https://gist.github.com/jamesknelson/ab93890eb26f2841a2f8846d4013b151
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Instantly share code, notes, and snippets.
import * as Govern from 'govern' import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios'
export interface HTTPOperation<Success = any, Rejection = any> { hasEnded: boolean isBusy: boolean wasCancelled: boolean wasSuccessful: boolean wasRejected: boolean
error?: any
/** * Data about the operation. For example, the requested data, or * details on why the operation failed or was cancelled, * * Note that requests do not need to make anything available on this * attribute, and may opt to store their received data in a global store * instead. * * Cannot be typed as you never know what will be returned by an error. */ response?: HTTPOperationResponse<any>
/** * Start an operation that was held due to lack of authentication or network * connectivity. */ start?: () => void }
export enum HTTPOperationStatus { // The request couldn't be completed due to lack of authentication, // and should be retried once the user has authenticated. // // The app should show an authentication modal when any requests have this // state, so UIs can just treat it as `busy`. AwaitingAuthentication = 'AwaitingAuthentication',
// The request couyldn't be completed due to lack of connection, // and should be retried once the user has reconnected. // // This isn't treated as a failed request, so optimistic UIs can // still display the expected result, but may also display a warning // that some changes may not be saved. AwaitingConnection = 'AwaitingConnection',
// The request is in progress Busy = 'Busy',
Cancelled = 'Cancelled', Error = 'Error', Rejection = 'Rejected', Success = 'Success', }
export namespace HTTPOperation { export const Status = HTTPOperationStatus export type Status = HTTPOperationStatus
export type Response<Data = any> = AxiosResponse<Data> }
export type HTTPOperationResponse<Data = any> = AxiosResponse<Data>
export interface HTTPOperationControllerProps<Success = any, Rejection = any> extends AxiosRequestConfig { maxRetries?: number
/** * If false, a failure to authenticate will be treated as a failure instead * of an indication that the user needs to authenticate. */ canAwaitAuthentication?: boolean
/** * If false, a lack of connection will prevent the request from being * retried. */ canAwaitConnection?: boolean
/** * If true, prevents the request from being made. */ isAwaitingAuthentication?: boolean
/** * If false, prevents the request from being made. The exponential backoff * timing is reset when it's value changes. */ isOnline?: boolean
/** * This is a convenience prop to help in situations where authentication * -related headers need to change between request tries. */ authenticationHeaders?: { [name: string]: string }
/** * Allows you to configure which responses are considered unauthenticated, * not connected, failed, and successful. */ getResponseStatus?(response: HTTPOperationResponse<any>): HTTPOperationStatus
/** * Called upon successful completion of the request. Use this to store * received data, update authentication headers, etc. */ onSuccess?(response: HTTPOperationResponse<Success>): void
/** * The request failed for an unknown reason, e.g. a 5xx status code. */ onError?(response?: HTTPOperationResponse<any>): void
/** * The request was rejected by the server, e.g. a 4xx status code. */ onRejection?(response?: HTTPOperationResponse<Rejection>): void
/** * Called after success, failure, rejection or cancelled. Facilitates cleanup that must * be run regardless of the result. */ onEnd?(response?: HTTPOperationResponse<any>): void
/** * The request was cancelled before it could be completed. This is called * if a request can't be made due to network or authentication issues, and * is cancelled before it can be retried. **/ onCancel?(): void
/** * A request is about to be sent. */ onSend?(): void
/** * You cannot supply axios' "validateStatus" option, as "request" uses it * internally. */ validateStatus?: never }
interface HTTPOperationControllerState { status: HTTPOperationStatus hasEnded: boolean response?: HTTPOperationResponse<any> }
export class HTTPOperationController< Success = any, Rejection = any > extends Govern.Component< HTTPOperationControllerProps<Success, Rejection>, HTTPOperationControllerState, HTTPOperation<Success, Rejection> > { static Element<Success = any, Rejection = any>( props: HTTPOperationControllerProps<Success, Rejection>, ) { return Govern.createElement( HTTPOperationController as Govern.ElementType< HTTPOperationController<Success, Rejection> >, props, ) }
static defaultProps = { canAwaitAuthentication: true, canAwaitConnection: true, getResponseStatus: defaultGetResponseStatus, }
axiosConfig: AxiosRequestConfig holdCount: number isDisposed: boolean
constructor(props) { super(props)
this.holdCount = 0
let { maxDisconnectedRetries, maxUnauthenticatedRetries, initialStatus, getResponseStatus, getResult, getError, requestDidSucceed, requestDidFail, requestDidComplete, requestWasCancelled, requestWasHeld, requestWillSend, ...axiosConfig } = props
this.axiosConfig = axiosConfig
// We want to allow configuration of response status via // `getResponseStatus`, so always have axios return success (except when // there is no connection) this.axiosConfig.validateStatus = () => true
// Set state to "Busy" *after* calling `this.start`, so that `this.start` // doesn't find "Busy" as the status and then throw an error. this.state = { hasEnded: false, status: initialStatus, } }
render() { return { isBusy: this.state.status === HTTPOperationStatus.Busy, hasEnded: this.state.hasEnded,
wasCancelled: this.state.status === HTTPOperationStatus.Cancelled, wasSuccessful: this.state.status === HTTPOperationStatus.Success, wasRejected: this.state.status === HTTPOperationStatus.Rejection,
error: this.state.status === HTTPOperationStatus.Error,
/** * The result of the action. Only available when status is "success". */ response: this.state.response,
/** * Start or restart an action. */ start: this.start, } }
componentDidMount() { if (!this.state.status) { this.start() } }
componentDidUpdate(prevProps: HTTPOperationControllerProps<any>) { if ( prevProps.isOnline === false && this.props.isOnline === true && this.state.status === HTTPOperationStatus.AwaitingConnection ) { this.start() } }
componentWillUnmount() { this.isDisposed = true }
start = () => { let props = this.props let status = this.state.status
if ( status && status !== HTTPOperationStatus.AwaitingAuthentication && status !== HTTPOperationStatus.AwaitingConnection ) { console.error( `You can only start an "AwaitingAuthentication" and "AwaitingConnection" requests, but the request to "${ props.url }" has status "${status}".`, ) return }
// The `requestWillSend` callback and `setState` may both publish new // values, so run them in a single dispatcher action. this.dispatch(this.doRequest) } doRequest = () => { let attemptConfig = Object.assign({}, this.axiosConfig)
if (this.props.authenticationHeaders) { attemptConfig.headers = Object.assign( {}, this.axiosConfig.headers, this.props.authenticationHeaders, ) }
// Run the "requestWillSend" lifecycle method before updating state // and notifying subscribers that we're sending. if (this.props.onSend) { this.props.onSend() }
this.setState({ status: HTTPOperationStatus.Busy, })
axios .request(attemptConfig) .then(this.handleAxiosResponse, this.handleAxiosError) }
cancel = () => { let props = this.props let status = this.state.status
if ( status !== HTTPOperationStatus.AwaitingAuthentication && status !== HTTPOperationStatus.AwaitingConnection && status !== HTTPOperationStatus.Busy ) { console.error( `You can only cancel "AwaitingAuthentication", "AwaitingConnection" or "Busy" requests, but the request to "${ props.url }" has status "${status}".`, ) return } if (status === HTTPOperationStatus.Busy) { console.warn( `Cancelling in-progress HTTP requests is only partially supported; the request will continue, but the result will not be processed.`, ) }
// The `requestWasCancelled` callback and `setState` may both publish new // values, so run them in a single dispatcher action. this.dispatch(() => { this.setState({ hasEnded: true, status: HTTPOperationStatus.Cancelled, }) if (this.props.onCancel) { this.props.onCancel() } if (this.props.onEnd) { this.props.onEnd() } }) }
hold = (status: HTTPOperationStatus, response?: AxiosResponse<any>) => { let props = this.props
++this.holdCount let maxRetries = props.maxRetries === undefined ? 0 : props.maxRetries
if ( (this.holdCount <= maxRetries && (props.canAwaitAuthentication && status === HTTPOperationStatus.AwaitingAuthentication)) || (props.canAwaitConnection && status === HTTPOperationStatus.AwaitingConnection) ) { this.dispatch(() => { // We haven't yet reached our retry limit. this.setState({ status: status, }) }) } else { this.error(response) } }
error = (response?: AxiosResponse<any>) => { this.dispatch(() => { this.setState({ hasEnded: true, status: HTTPOperationStatus.Error, response, }) if (this.props.onError) { this.props.onError(response) } if (this.props.onEnd) { this.props.onEnd(response) } }) }
handleAxiosResponse = (response: AxiosResponse<any>) => { let props = this.props
if (this.isDisposed) { // The request may be disposed before we receive a response if it // is cancelled. return }
if (this.state.status !== HTTPOperationStatus.Cancelled) { let status = this.props.getResponseStatus!(response) switch (status) { case HTTPOperationStatus.Cancelled: return this.cancel()
case HTTPOperationStatus.Success: this.dispatch(() => { this.setState({ response, hasEnded: true, status, }) if (this.props.onSuccess) { this.props.onSuccess(response) } if (this.props.onEnd) { this.props.onEnd(response) } }) break
case HTTPOperationStatus.Rejection: this.dispatch(() => { this.setState({ response, hasEnded: true, status, }) if (this.props.onRejection) { this.props.onRejection(response) } if (this.props.onEnd) { this.props.onEnd(response) } }) break
case HTTPOperationStatus.Error: this.error(response) break
case HTTPOperationStatus.AwaitingAuthentication: case HTTPOperationStatus.AwaitingConnection: this.hold(status, response) break
default: throw new Error( `getResponseStatus returned unknow status "${status}".`, ) } } }
protected handleAxiosError = error => { if (this.isDisposed) { // The request may be disposed before we receive a response if it // is cancelled. return }
if (this.state.status !== HTTPOperationStatus.Cancelled) { // I'm assuming that any Axios errors indicate lack of network // connectivity. If this is incorrect, PRs with fixes are welcome! this.hold(HTTPOperationStatus.AwaitingConnection) } } }
function defaultGetResponseStatus( response: HTTPOperationResponse<any>, ): HTTPOperationStatus { if (!response.status) { return HTTPOperationStatus.AwaitingConnection } else if (response.status === 401) { return HTTPOperationStatus.AwaitingAuthentication } else if (response.status >= 200 && response.status < 300) { return HTTPOperationStatus.Success } else if (response.status >= 400 && response.status < 500) { return HTTPOperationStatus.Rejection } else { return HTTPOperationStatus.Error } }
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK