toggle-corp/react-rest-request

View on GitHub
src/RequestCoordinator.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';

import { CoordinatorAttributes, Context } from './declarations';
import { RequestContext } from './RequestContext';
import {
    RestRequest,
    prepareUrlParams,
    HandlerFunc,
} from './RestRequest';

const emptyObject = {};

/*
interface Verbosity {
    showLog?: boolean;
    showError?: boolean;
    showWarning?: boolean;

    showRequest?: boolean;
    showResponse?: boolean;
}
*/

interface Attributes<Props, NewProps>{
    // verbosity: Verbosity;
    transformUrl?(url: string, data: CoordinatorAttributes, props: Props): string;
    transformProps(props: Props): NewProps;
    transformParams(data: CoordinatorAttributes, props: Props): object;
    transformResponse?(body: object, data: CoordinatorAttributes): object;
    transformErrors?(body: object, data: CoordinatorAttributes): object;
    onFailure?(value: { error: object; status: number }): void;
    onFatal?(value: { error: object }): void;
}

interface Request {
    running: boolean;
    data: CoordinatorAttributes;
    stop(): void;
    start(): void;
}

// eslint-disable-next-line import/prefer-default-export, max-len
export const createRequestCoordinator = <Props, NewProps>(attributes: Attributes<Props, NewProps>) => (WrappedComponent: React.ComponentType<NewProps>) => {
    const {
        transformParams,
        transformResponse,
        transformErrors,
        transformProps,
        transformUrl,
        onFailure: onFailureDefault,
        onFatal: onFatalDefault,
    } = attributes;

    class Coordinator extends React.Component<Props, Context['state']> {
        public constructor(props: Props) {
            super(props);
            this.state = {};
            // store information about every request
            // Coordinator updates response, responseError and responseStatus
        }

        public componentDidMount() {
            this.mounted = true;
            this.forEachRequest(request => request.start());
        }

        public componentWillUnmount() {
            this.forEachRequest(request => request.stop());
            this.mounted = false;
        }

        private mounted: boolean = false;

        private requests: { [key: string]: Request } = {};

        private requestGroups: { [key: string]: string[] } = {};

        private forEachRequest = (callback: (data: Request) => void) => {
            Object.keys(this.requests).forEach((key) => {
                callback(this.requests[key]);
            });
        }

        // called as api by children
        private stopRequest: Context['stopRequest'] = (key) => {
            this.setState({ [key]: { pending: false } }, () => {
                const request = this.requests[key];
                if (request) {
                    request.stop();
                }
            });
        }

        // called as api by children
        private startRequest: Context['startRequest'] = (requestData, ignoreIfExists) => {
            const {
                key,
                group,
                url,
                query,
                options = {},
            } = requestData;

            const oldRequest = this.requests[key];

            if (oldRequest && oldRequest.running) {
                if (ignoreIfExists) {
                    return;
                }
                oldRequest.stop();
            }

            const calculateParams = () => (
                transformParams(requestData, this.props)
            );

            const appendage = query && prepareUrlParams(query);
            const preparedUrl = appendage && appendage.length > 0 ? `${url}?${appendage}` : url;

            const request = new RestRequest({
                ...options,
                key,
                url: transformUrl ? transformUrl(preparedUrl, requestData, this.props) : preparedUrl,
                params: calculateParams,
                onPreLoad: this.handleRequestStart,
                onInitialize: this.handleRequestStart,
                onAfterLoad: this.handleRequestDone,
                onAbort: this.handleRequestDone,
                onSuccess: this.handleSuccess,
                onFailure: this.handleFailure,
                onFatal: this.handleFatal,
            });

            this.requests[key] = {
                data: requestData,
                running: false,
                stop: () => {
                    request.stop();
                    this.requests[key].running = false;
                },
                start: () => {
                    this.requests[key].running = true;
                    request.start();
                },
            };

            if (group) {
                if (!this.requestGroups[group]) {
                    this.requestGroups[group] = [key];
                } else {
                    this.requestGroups[group].push(key);
                }
            }

            if (this.mounted) {
                this.requests[key].start();
            }
        }

        private handleRequestStart = (key: string) => {
            const newState = { pending: true };

            // Calculate group state
            const { data: { group } } = this.requests[key];
            const groupState = group ? {
                [group]: {
                    pending: true,
                },
            } : emptyObject;

            this.setState({ [key]: newState, ...groupState });
        }

        private handleRequestDone = (key: string) => {
            const { state } = this;
            const requestState = state[key] || emptyObject;
            const newState = {
                ...requestState,
                pending: false,
            };

            // Calculate group state
            const { data: { group } } = this.requests[key];
            const groupState = group ? {
                [group]: {
                    pending: this.requestGroups[group]
                        .filter(k => k !== key)
                        .some(k => !!state[k].pending),
                },
            } : emptyObject;

            this.setState({ [key]: newState, ...groupState });
        }

        private handleSuccess: HandlerFunc = (key, body, status) => {
            const { data } = this.requests[key];

            let response;
            try {
                response = transformResponse ? transformResponse(body, data) : body;
            } catch (e) {
                this.handleFatal(key, e, 0);
                return;
            }

            const { onSuccess } = data;
            if (onSuccess) {
                onSuccess({ response, status });
            }

            const { state } = this;
            const requestState = state[key] || emptyObject;
            const newState = {
                ...requestState,
                response,
                responseError: undefined,
                responseStatus: status,
            };
            this.setState({ [key]: newState });
        }

        private handleFailure: HandlerFunc = (key, body, status) => {
            const { data } = this.requests[key];

            let error;
            try {
                error = transformErrors ? transformErrors(body, data) : body;
            } catch (e) {
                this.handleFatal(key, e, 0);
                console.error(e);
                return;
            }

            const { onFailure } = data;
            if (onFailure) {
                onFailure({ error, status });
            } else if (onFailureDefault) {
                onFailureDefault({ error, status });
            }

            const { state } = this;
            const requestState = state[key] || emptyObject;
            const newState = {
                ...requestState,
                response: undefined,
                responseError: error,
                responseStatus: status,
            };
            this.setState({ [key]: newState });
        }

        private handleFatal: HandlerFunc = (key, error, status) => {
            const { data } = this.requests[key];
            const { onFatal } = data;
            if (onFatal) {
                onFatal({ error });
            } else if (onFatalDefault) {
                onFatalDefault({ error });
            }

            const { state } = this;
            const requestState = state[key] || emptyObject;
            const newState = {
                ...requestState,
                response: undefined,
                responseError: error,
                responseStatus: status,
            };
            this.setState({ [key]: newState });
        }

        public render() {
            const contextApi = {
                startRequest: this.startRequest,
                stopRequest: this.stopRequest,
                state: this.state,
            };

            const newProps = transformProps(this.props);

            return (
                <RequestContext.Provider value={contextApi}>
                    <WrappedComponent {...newProps} />
                </RequestContext.Provider>
            );
        }
    }

    return hoistNonReactStatics<React.ComponentType<Props>, React.ComponentType<NewProps>>(
        Coordinator,
        WrappedComponent,
    );
};