superdesk/superdesk-client-core

View on GitHub
scripts/core/data/index.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import {createStore} from 'redux';
import {IBaseRestApiResponse, IResourceUpdateEvent, IUser, IWebsocketMessage} from 'superdesk-api';
import {addWebsocketEventListener} from 'core/notification/notification';
import {getMiddlewares} from 'core/redux-utils';
import {debounceAsync, IDebounced} from 'core/helpers/debounce-async';
import {requestQueue} from 'core/helpers/request-queue';
import {keyBy} from 'lodash';

/**
 * To add a new entity, add it to
 * resourcesInStore, IInterfacesByResourceType
 * and call `initEntity`
 */

const resourcesInStore = [
    'users',
] as const;

interface IInterfacesByResourceType {
    users: IUser;
}

type IStoreResource = typeof resourcesInStore[number];

export type IStoreState = {
    entities: {
        [Properties in keyof IInterfacesByResourceType]: {
            [key: string]: IInterfacesByResourceType[Properties];
        };
    };
    subjectCodes: {[qcode: string]: {qcode: string; name: string; parent?: string}};
};

type IUpdateEntityAction = {
    type: 'UPDATE_ENTITY',
    payload: {
        resource: IStoreResource;
        _id: string;
        data: any;
    },
};

interface IInitEntityAction {
    type: 'INIT_ENTITY';
    payload: {
        resource: string;
        items: Array<any>;
    };
}

interface ILoadSubjectCodes {
    type: 'LOAD_SUBJECT_CODES';
    payload: IStoreState['subjectCodes'];
}

interface IUpdateLastUserActiveAction {
    type: 'UPDATE_USER_LAST_ACTIVE';
    payload: IUser['_id'];
}

type IUserAction = IUpdateLastUserActiveAction;

type IAction = IUserAction | IUpdateEntityAction | IInitEntityAction | ILoadSubjectCodes;

const initialState: IStoreState = {
    entities: {
        users: {},
    },
    subjectCodes: {},
};

function isStoreResource(resource: any): resource is IStoreResource {
    return resourcesInStore.includes(resource);
}

function reducer(state: IStoreState = initialState, action: IAction): IStoreState {
    switch (action.type) {
    /**
     * USER ACTIONS
     */
    case 'UPDATE_USER_LAST_ACTIVE':
        return (() => {
            const userId = action.payload;

            return {
                ...state,
                entities: {
                    ...state.entities,
                    users: {
                        ...state.entities.users,
                        [userId]: {
                            ...state.entities.users[userId],
                            last_activity_at: (new Date()).toISOString(),
                        },
                    },
                },
            };
        })();

    /**
     * OTHER ACTIONS
     */
    case 'LOAD_SUBJECT_CODES':
        return {
            ...state,
            subjectCodes: action.payload,
        };

    /**
     * GENERIC ACTIONS THAT APPLY TO ALL ENTITIES
     */
    case 'INIT_ENTITY':
        return (() => {
            const {resource, items} = action.payload;

            return {
                ...state,
                entities: {
                    ...state.entities,
                    [resource]: keyBy(items, (entity) => entity._id),
                },
            };
        })();

    case 'UPDATE_ENTITY':
        return (() => {
            const resource = action.payload.resource;
            const entity = action.payload.data;

            return {
                ...state,
                entities: {
                    ...state.entities,
                    [resource]: {
                        ...state.entities[resource],
                        [entity._id]: entity,
                    },
                },
            };
        })();

    default:
        return state;
    }
}

export const store = createStore<IStoreState, IAction, {}, {}>(reducer, getMiddlewares());

export function initEntity<T extends IStoreResource>(
    resource: T,
    items: Array<IInterfacesByResourceType[T]>,
) {
    store.dispatch({
        type: 'INIT_ENTITY',
        payload: {
            resource,
            items,
        },
    });
}

const updating: {[key: string]: IDebounced} = {};

addWebsocketEventListener(
    'resource:updated',
    (event: IWebsocketMessage<IResourceUpdateEvent>) => {
        const {resource, _id, fields} = event.extra;
        const fieldKeys = Object.keys(fields);

        if (!isStoreResource(resource)) {
            return;
        } else if (resource === 'users' &&
            fieldKeys.length === 1 &&
            fieldKeys[0] === 'last_activity_at' &&
            store.getState().entities.users[_id] != null
        ) {
            // This websocket message is purely to update a User's last_activity_at
            // So manually update the resource rather than sending an API request
            store.dispatch({
                type: 'UPDATE_USER_LAST_ACTIVE',
                payload: _id,
            });
            return;
        }

        const updateKey = resource + _id;

        if (updating[updateKey] != null) {
            updating[updateKey]();
        } else {
            const requestDebounced = debounceAsync(
                (abortController) => {
                    return requestQueue.add({
                        method: 'GET',
                        endpoint: `/${resource}/${_id}`,
                        abortSignal: abortController.signal,
                    }, store)
                        .then((data: IBaseRestApiResponse) => {
                            store.dispatch({
                                type: 'UPDATE_ENTITY',
                                payload: {
                                    resource,
                                    _id,
                                    data,
                                },
                            });
                        }).finally(() => {
                            delete updating[updateKey];
                        });
                },
                1000,
                3000,
            );

            updating[updateKey] = requestDebounced;
            updating[updateKey]();
        }
    },
);