Skip to content

Commit

Permalink
feat: extensive and smart caching
Browse files Browse the repository at this point in the history
closes #6
  • Loading branch information
satanTime committed Apr 12, 2020
1 parent 239f3de commit 713a610
Show file tree
Hide file tree
Showing 17 changed files with 946 additions and 235 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Imagine that we have the next models in `ngrx/store`:
export interface User {
id: string;
firstName: string;
firstLast: string;
lastName: string;

company?: Company;
companyId?: string;
Expand Down
91 changes: 70 additions & 21 deletions src/childEntity.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
CACHE,
CACHE_CHECKS_SET,
FEATURE_SELECTOR,
HANDLER_CACHE,
HANDLER_RELATED_ENTITY,
ID_FILTER_PROPS,
ID_SELECTOR,
ID_TYPES,
UNKNOWN,
VALUES_FILTER_PROPS,
} from './types';
import {normalizeSelector} from './utils';
import {mergeCache, normalizeSelector, verifyCache} from './utils';

export function childEntity<
STORE,
Expand All @@ -25,37 +26,85 @@ export function childEntity<
const {collection: funcSelector, id: idSelector} = normalizeSelector(featureSelector);

const callback = (
cachePrefix: string,
cacheLevel: string,
state: STORE,
cacheRefs: HANDLER_CACHE<STORE, UNKNOWN>,
cache: CACHE<STORE>,
source: PARENT_ENTITY,
idParentSelector: ID_SELECTOR<PARENT_ENTITY>,
) => {
// a bit magic to relax generic types.
let relatedId: undefined | ID_TYPES;
const stateItems = funcSelector(state).entities;
for (const stateItem of Object.values(stateItems)) {
if (!stateItem || stateItem[keyId] !== (idParentSelector(source) as any) /* todo fix any A8 */) {
continue;
const featureState = funcSelector(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 [idChecks, id]: [CACHE_CHECKS_SET<STORE>, ID_TYPES | undefined] = cacheDataLevel.get(`!${parentId}`) || [
new Map(),
undefined,
];
if (!verifyCache(state, idChecks)) {
id = undefined;
for (const entity of Object.values(featureState.entities)) {
if (
!entity ||
entity[keyId] !== ((parentId as any) as RELATED_ENTITY[RELATED_KEY_IDS]) // todo fix any A8
) {
continue;
}
id = idSelector(entity);
break;
}
relatedId = idSelector(stateItem);
break;
idChecks = new Map();
idChecks.set(funcSelector, new Map());
idChecks.get(funcSelector)?.set(null, featureState.entities);
cacheDataLevel.set(`!${parentId}`, [idChecks, id]);
}
if (!relatedId) {
cacheRefs.push([cachePrefix, funcSelector, null, stateItems]);
return;
if (!id) {
return `!${parentId}`;
}

const cacheHash = `#${id}`;
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 = undefined;
checks = new Map();
checks.set(funcSelector, new Map());
checks.get(funcSelector)?.set(null, featureState.entities);
checks.get(funcSelector)?.set(id, featureState.entities[id]);

// the entity does not exist.
if (!featureState.entities[id]) {
cacheDataLevel.set(cacheHash, [checks, value]);
return cacheHash;
}

// we have to clone it because we are going to update it with relations.
const cacheValue = {...stateItems[relatedId]} as RELATED_ENTITY;
cacheRefs.push([cachePrefix, funcSelector, relatedId, stateItems[relatedId], cacheValue]);
value = {...featureState.entities[id]} as RELATED_ENTITY;

let incrementedPrefix = 0;
let cacheRelLevelIndex = 0;
for (const relationship of relationships) {
incrementedPrefix += 1;
relationship(`${cachePrefix}:${incrementedPrefix}`, state, cacheRefs, cacheValue, idSelector);
const cacheRelLevel = `${cacheLevel}:${cacheRelLevelIndex}`;
const cacheRelHash = relationship(cacheRelLevel, state, cache, value, idSelector);
cacheRelLevelIndex += 1;
if (cacheRelHash) {
mergeCache(cache.get(cacheRelLevel)?.get(cacheRelHash)?.[0], checks);
}
}
source[keyValue] = (cacheValue as any) as PARENT_ENTITY[RELATED_KEY_VALUES];
cacheDataLevel.set(cacheHash, [checks, value]);
source[keyValue] = value as PARENT_ENTITY[RELATED_KEY_VALUES];
return cacheHash;
};
callback.ngrxEntityRelationship = 'childEntity';
callback.idSelector = idSelector;
Expand Down
115 changes: 92 additions & 23 deletions src/childrenEntities.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
CACHE,
CACHE_CHECKS_SET,
FEATURE_SELECTOR,
HANDLER_CACHE,
HANDLER_RELATED_ENTITY,
ID_FILTER_PROPS,
ID_SELECTOR,
ID_TYPES,
UNKNOWN,
VALUES_FILTER_PROPS,
} from './types';
import {normalizeSelector} from './utils';
import {mergeCache, normalizeSelector, verifyCache} from './utils';

export function childrenEntities<
STORE,
Expand All @@ -23,46 +24,114 @@ export function childrenEntities<
...relationships: Array<HANDLER_RELATED_ENTITY<STORE, RELATED_ENTITY>>
): HANDLER_RELATED_ENTITY<STORE, PARENT_ENTITY> {
const {collection: funcSelector, id: idSelector} = normalizeSelector(featureSelector);
const emptyResult: Map<UNKNOWN, UNKNOWN> = new Map();

const callback = (
cachePrefix: string,
cacheLevel: string,
state: STORE,
cacheRefs: HANDLER_CACHE<STORE, UNKNOWN>,
cache: CACHE<STORE>,
source: PARENT_ENTITY,
idParentSelector: ID_SELECTOR<PARENT_ENTITY>,
) => {
// a bit magic to relax generic types.
const relatedIds: Array<ID_TYPES> = [];
const stateItems = funcSelector(state).entities;
for (const stateItem of Object.values(stateItems)) {
if (!stateItem || stateItem[keyId] !== (idParentSelector(source) as any) /* todo fix any A8 */) {
continue;
const featureState = funcSelector(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 Object.values(featureState.entities)) {
if (
!entity ||
entity[keyId] !== ((parentId as any) as RELATED_ENTITY[RELATED_KEY_IDS]) // todo fix any A8
) {
continue;
}
const id = idSelector(entity);
if (id) {
ids.push(id);
}
}
relatedIds.push(idSelector(stateItem));
idsChecks = new Map();
idsChecks.set(funcSelector, new Map());
idsChecks.get(funcSelector)?.set(null, featureState.entities);
cacheDataLevel.set(`!${parentId}`, [idsChecks, ids]);
}
if (!ids.length) {
source[keyValue] = emptyResult.get(parentId);
if (!source[keyValue]) {
source[keyValue] = [] as PARENT_ENTITY[RELATED_KEY_VALUES_ARRAYS];
emptyResult.set(parentId, source[keyValue]);
}
return `!${parentId}`;
}

const relatedItems: Array<RELATED_ENTITY> = [];
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;
}

for (const id of relatedIds) {
// we have to clone it because we are going to update it with relations.
const cacheValue = {...stateItems[id]} as RELATED_ENTITY;
// building a new value.
value = [];
checks = new Map();
checks.set(funcSelector, new Map());
checks.get(funcSelector)?.set(null, featureState.entities);
for (const id of ids) {
checks.get(funcSelector)?.set(id, featureState.entities[id]);
}

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

let incrementedPrefix = 0;
let cacheRelLevelIndex = 0;
for (const relationship of relationships) {
incrementedPrefix += 1;
relationship(`${cachePrefix}:${incrementedPrefix}`, state, cacheRefs, cacheValue, idSelector);
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);
}
}
relatedItems.push(cacheValue);
cacheDataLevel.set(`${cacheHash}:${id}`, [entityChecks, entityValue]);
value.push(entityValue);
}

cacheRefs.push([cachePrefix, funcSelector, null, stateItems]);
source[keyValue] = relatedItems as PARENT_ENTITY[RELATED_KEY_VALUES_ARRAYS];
cacheDataLevel.set(cacheHash, [checks, value]);
source[keyValue] = value as PARENT_ENTITY[RELATED_KEY_VALUES_ARRAYS];
return cacheHash;
};
callback.ngrxEntityRelationship = 'childrenEntities';
callback.idSelector = idSelector;
callback.release = () => {
emptyResult.clear();
for (const relationship of relationships || []) {
relationship.release();
}
Expand Down
Loading

0 comments on commit 713a610

Please sign in to comment.