1

HTTPOperation.ts · GitHub

 3 years ago
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.
HTTPOperation.ts · GitHub

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 } }


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK