satanTime/ngrx-entity-relationship

View on GitHub
libs/ngrx-entity-relationship/src/lib/childrenEntities.ts

Summary

Maintainability
D
2 days
Test Coverage
A
100%
import {
    CACHE,
    CACHE_CHECKS_SET,
    FEATURE_SELECTOR,
    HANDLER_RELATED_ENTITY,
    ID_FILTER_PROPS,
    ID_SELECTOR,
    ID_TYPES,
    isSelectorMeta,
    UNKNOWN,
    VALUES_FILTER_PROPS,
} from './types';
import {argsToArray, mergeCache, normalizeSelector, objectValues, verifyCache} from './utils';

export function childrenEntities<
    STORE,
    PARENT_ENTITY,
    RELATED_ENTITY,
    RELATED_KEY_IDS extends ID_FILTER_PROPS<RELATED_ENTITY, ID_TYPES> = ID_FILTER_PROPS<RELATED_ENTITY, ID_TYPES>,
    RELATED_KEY_VALUES_ARRAYS extends VALUES_FILTER_PROPS<PARENT_ENTITY, Array<RELATED_ENTITY>> = VALUES_FILTER_PROPS<
        PARENT_ENTITY,
        Array<RELATED_ENTITY>
    >,
>(
    featureSelector: FEATURE_SELECTOR<STORE, RELATED_ENTITY>,
    keyId: RELATED_KEY_IDS,
    keyValue: RELATED_KEY_VALUES_ARRAYS,
    metaOrRelationship?: SELECTOR_META | HANDLER_RELATED_ENTITY<STORE, RELATED_ENTITY>,
    ...relationships: Array<HANDLER_RELATED_ENTITY<STORE, RELATED_ENTITY>>
): HANDLER_RELATED_ENTITY<STORE, PARENT_ENTITY>;

export function childrenEntities<
    STORE,
    PARENT_ENTITY,
    RELATED_ENTITY,
    RELATED_KEY_IDS extends ID_FILTER_PROPS<RELATED_ENTITY, ID_TYPES>,
    RELATED_KEY_VALUES_ARRAYS extends VALUES_FILTER_PROPS<PARENT_ENTITY, Array<RELATED_ENTITY>>,
>(
    featureSelector: FEATURE_SELECTOR<STORE, RELATED_ENTITY>,
    keyId: RELATED_KEY_IDS,
    keyValue: RELATED_KEY_VALUES_ARRAYS,
): HANDLER_RELATED_ENTITY<STORE, PARENT_ENTITY> {
    let relationships: Array<HANDLER_RELATED_ENTITY<STORE, RELATED_ENTITY>> = argsToArray(arguments);
    relationships = relationships.slice(3);

    let meta: SELECTOR_META = {};
    if (isSelectorMeta(relationships[0])) {
        meta = relationships[0];
        relationships = relationships.slice(1);
    }

    const {collection: collectionSelector, id: idSelector} = normalizeSelector(featureSelector);
    const emptyResult: Map<UNKNOWN, UNKNOWN> = new Map();

    const callback = (
        cacheLevel: string,
        state: STORE,
        cache: CACHE<STORE>,
        source: PARENT_ENTITY,
        idParentSelector: ID_SELECTOR<PARENT_ENTITY>,
    ) => {
        const featureState = collectionSelector(state);
        const parentId = idParentSelector(source);

        let cacheDataLevel = cache.get(cacheLevel);
        if (!cacheDataLevel) {
            cacheDataLevel = new Map();
            cache.set(cacheLevel, cacheDataLevel);
        }

        // maybe we don't need to scan the entities.
        let [idsChecks, ids]: [CACHE_CHECKS_SET<STORE>, Array<ID_TYPES>] = cacheDataLevel.get(`!${parentId}`) || [
            new Map(),
            [],
        ];
        if (!verifyCache(state, idsChecks)) {
            ids = [];
            for (const entity of objectValues(featureState.entities)) {
                if (
                    !entity ||
                    entity[keyId] !== (parentId as any as RELATED_ENTITY[RELATED_KEY_IDS]) // todo fix any A8
                ) {
                    continue;
                }
                const id = idSelector(entity);
                // istanbul ignore else
                if (id) {
                    ids.push(id);
                }
            }
            idsChecks = new Map();
            const idsChecksEntities = new Map();
            idsChecks.set(collectionSelector, idsChecksEntities);
            idsChecksEntities.set(null, featureState.entities);
            cacheDataLevel.set(`!${parentId}`, [idsChecks, ids]);
        }
        if (!ids.length) {
            source[keyValue] = emptyResult.get(parentId);
            // istanbul ignore else
            if (!source[keyValue]) {
                source[keyValue] = [] as PARENT_ENTITY[RELATED_KEY_VALUES_ARRAYS];
                emptyResult.set(parentId, source[keyValue]);
            }
            return `!${parentId}`;
        }

        const cacheHash = `#${ids.join(',')}`;
        let [checks, value]: [CACHE_CHECKS_SET<STORE>, UNKNOWN] = cacheDataLevel.get(cacheHash) || [
            new Map(),
            undefined,
        ];
        if (verifyCache(state, checks)) {
            source[keyValue] = value;
            return cacheHash;
        }

        // building a new value.
        value = [];
        checks = new Map();
        const checksEntities = new Map();
        checks.set(collectionSelector, checksEntities);
        checksEntities.set(null, featureState.entities);
        for (const id of ids) {
            checksEntities.set(id, featureState.entities[id]);
        }

        for (const id of ids) {
            let [entityChecks, entityValue]: [CACHE_CHECKS_SET<STORE>, UNKNOWN] = cacheDataLevel.get(
                `${cacheHash}:${id}`,
            ) || [new Map(), undefined];
            if (verifyCache(state, entityChecks)) {
                // istanbul ignore else
                if (entityValue) {
                    value.push(entityValue);
                }
                continue;
            }
            // we have to clone it because we are going to update it with relationships.
            entityValue = {...featureState.entities[id]} as RELATED_ENTITY;
            entityChecks = new Map();
            const entityChecksEntities = new Map();
            entityChecks.set(collectionSelector, entityChecksEntities);
            entityChecksEntities.set(null, featureState.entities);
            entityChecksEntities.set(id, featureState.entities[id]);

            let cacheRelLevelIndex = 0;
            for (const relationship of relationships) {
                const cacheRelLevel = `${cacheLevel}:${cacheRelLevelIndex}`;
                const cacheRelHash = relationship(cacheRelLevel, state, cache, entityValue, idSelector);
                cacheRelLevelIndex += 1;
                if (cacheRelHash) {
                    mergeCache(cache.get(cacheRelLevel)?.get(cacheRelHash)?.[0], checks);
                    mergeCache(cache.get(cacheRelLevel)?.get(cacheRelHash)?.[0], entityChecks);
                }
            }
            cacheDataLevel.set(`${cacheHash}:${id}`, [entityChecks, entityValue]);
            value.push(entityValue);
        }
        cacheDataLevel.set(cacheHash, [checks, value]);
        source[keyValue] = value as PARENT_ENTITY[RELATED_KEY_VALUES_ARRAYS];
        return cacheHash;
    };
    callback.ngrxEntityRelationship = 'childrenEntities';
    callback.collectionSelector = collectionSelector;
    callback.meta = meta;
    callback.idSelector = idSelector;
    callback.relationships = relationships;
    callback.keyId = keyId;
    callback.keyValue = keyValue;
    callback.release = () => {
        emptyResult.clear();
        for (const relationship of relationships) {
            relationship.release();
        }
    };

    return callback;
}