@data-client/normalizr
Version:
Normalizes and denormalizes JSON according to schema for Redux and Flux applications
932 lines (869 loc) • 29.5 kB
JavaScript
'use strict';
class LocalCache {
localCache = new Map();
getEntity(pk, schema, entity, computeValue) {
const key = schema.key;
if (!this.localCache.has(key)) {
this.localCache.set(key, new Map());
}
const localCacheKey = this.localCache.get(key);
if (!localCacheKey.get(pk)) {
computeValue(localCacheKey);
}
return localCacheKey.get(pk);
}
getResults(input, cachable, computeValue) {
return {
data: computeValue(),
paths: []
};
}
}
const INVALID = Symbol('INVALID');
const UNDEF = {};
function isEntity(schema) {
return schema !== null && schema.pk !== undefined;
}
const validateSchema = definition => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const isArray = Array.isArray(definition);
if (isArray && definition.length > 1) {
throw new Error(`Expected schema definition to be a single schema, but found ${definition.length}.`);
}
}
return definition[0];
};
const getValues = input => Array.isArray(input) ? input : Object.keys(input).map(key => input[key]);
const filterEmpty = item => item !== undefined && typeof item !== 'symbol';
const normalize$2 = (schema, input, parent, key, args, visit) => {
schema = validateSchema(schema);
const values = getValues(input);
// Special case: Arrays pass *their* parent on to their children, since there
// is not any special information that can be gathered from themselves directly
return values.map(value => visit(schema, value, parent, key, args));
};
const denormalize$2 = (schema, input, args, unvisit) => {
schema = validateSchema(schema);
return input.map ? input.map(entityOrId => unvisit(schema, entityOrId)).filter(filterEmpty) : input;
};
function queryKey$1() {
return undefined;
}
/**
* Helpers to enable Immutable compatibility *without* bringing in
* the 'immutable' package as a dependency.
*/
/**
* Check if an object is immutable by checking if it has a key specific
* to the immutable library.
*
* @param {any} object
* @return {bool}
*/
function isImmutable(object) {
return !!(typeof object.hasOwnProperty === 'function' && (Object.hasOwnProperty.call(object, '__ownerID') ||
// Immutable.Map
object._map && Object.hasOwnProperty.call(object._map, '__ownerID'))); // Immutable.Record
}
/**
* Denormalize an immutable entity.
*
* @param {Schema} schema
* @param {Immutable.Map|Immutable.Record} input
* @param {function} unvisit
* @param {function} getDenormalizedEntity
* @return {Immutable.Map|Immutable.Record}
*/
function denormalizeImmutable(schema, input, args, unvisit) {
let deleted = false;
const obj = Object.keys(schema).reduce((object, key) => {
// Immutable maps cast keys to strings on write so we need to ensure
// we're accessing them using string keys.
const stringKey = `${key}`;
const item = unvisit(schema[stringKey], object.get(stringKey));
if (typeof item === 'symbol') {
deleted = true;
}
if (object.has(stringKey)) {
return object.set(stringKey, item);
} else {
return object;
}
}, input);
return deleted ? INVALID : obj;
}
const normalize$1 = (schema, input, parent, key, args, visit) => {
const object = {
...input
};
const keys = Object.keys(schema);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const localSchema = schema[k];
const value = visit(localSchema, input[k], input, k, args);
if (value === undefined) {
delete object[k];
} else {
object[k] = value;
}
}
return object;
};
const denormalize$1 = (schema, input, args, unvisit) => {
if (isImmutable(input)) {
return denormalizeImmutable(schema, input, args, unvisit);
}
const object = {
...input
};
let deleted = false;
const keys = Object.keys(schema);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const item = unvisit(schema[k], object[k]);
if (object[k] !== undefined) {
object[k] = item;
}
if (typeof item === 'symbol') {
deleted = true;
}
}
return deleted ? INVALID : object;
};
function queryKey(schema, args, unvisit, delegate) {
const resultObject = {};
for (const k of Object.keys(schema)) {
resultObject[k] = unvisit(schema[k], args, delegate);
}
return resultObject;
}
const getUnvisitEntity = (getEntity, cache, args, unvisit) => {
return function unvisitEntity(schema, entityOrId) {
const inputIsId = typeof entityOrId !== 'object';
const entity = inputIsId ? getEntity({
key: schema.key,
pk: entityOrId
}) : entityOrId;
if (typeof entity === 'symbol') {
return schema.denormalize(entity, args, unvisit);
}
if (entity === undefined && inputIsId &&
// entityOrId cannot be undefined literal as this function wouldn't be called in that case
// however the blank strings can still occur
entityOrId !== '' && entityOrId !== 'undefined') {
// we cannot perform WeakMap lookups with `undefined`, so we use a special object to represent undefined
// we're actually using this call to ensure we update the cache if a nested schema changes from `undefined`
// this is because cache.getEntity adds this key,pk as a dependency of anything it is nested under
return cache.getEntity(entityOrId, schema, UNDEF, localCacheKey => {
localCacheKey.set(entityOrId, undefined);
});
}
if (typeof entity !== 'object' || entity === null) {
return entity;
}
let pk = inputIsId ? entityOrId : schema.pk(entity, undefined, undefined, args);
// if we can't generate a working pk we cannot do cache lookups properly,
// so simply denormalize without caching
if (pk === undefined || pk === '' || pk === 'undefined') {
return noCacheGetEntity(localCacheKey => unvisitEntityObject(schema, entity, '', localCacheKey, args, unvisit));
}
// just an optimization to make all cache usages of pk monomorphic
if (typeof pk !== 'string') pk = `${pk}`;
// last function computes if it is not in any caches
return cache.getEntity(pk, schema, entity, localCacheKey => unvisitEntityObject(schema, entity, pk, localCacheKey, args, unvisit));
};
};
function unvisitEntityObject(schema, entity, pk, localCacheKey, args, unvisit) {
const entityCopy = schema.createIfValid(entity);
if (entityCopy === undefined) {
// undefined indicates we should suspense (perhaps failed validation)
localCacheKey.set(pk, INVALID);
} else {
// set before we recurse to prevent cycles causing infinite loops
localCacheKey.set(pk, entityCopy);
// we still need to set in case denormalize recursively finds INVALID
localCacheKey.set(pk, schema.denormalize(entityCopy, args, unvisit));
}
}
function noCacheGetEntity(computeValue) {
const localCacheKey = new Map();
computeValue(localCacheKey);
return localCacheKey.get('');
}
const getUnvisit = (getEntity, cache, args) => {
// we don't inline this as making this function too big inhibits v8's JIT
const unvisitEntity = getUnvisitEntity(getEntity, cache, args, unvisit);
function unvisit(schema, input) {
if (!schema) return input;
if (input === null || input === undefined) {
return input;
}
if (typeof schema.denormalize !== 'function') {
// deserialize fields (like Temporal.Instant)
if (typeof schema === 'function') {
return schema(input);
}
// shorthand for object, array
if (typeof schema === 'object') {
const method = Array.isArray(schema) ? denormalize$2 : denormalize$1;
return method(schema, input, args, unvisit);
}
} else {
if (isEntity(schema)) {
return unvisitEntity(schema, input);
}
return schema.denormalize(input, args, unvisit);
}
return input;
}
return (schema, input) => {
// in the case where WeakMap cannot be used
// this test ensures null is properly excluded from WeakMap
const cachable = Object(input) === input && Object(schema) === schema;
return cache.getResults(input, cachable, () => unvisit(schema, input));
};
};
/** Basic state interfaces for normalize side */
class BaseDelegate {
constructor({
entities,
indexes
}) {
this.entities = entities;
this.indexes = indexes;
}
// we must expose the entities object to track in our WeakDependencyMap
// however, this should not be part of the public API
// only used in buildQueryKey
tracked(schema) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const base = this;
const dependencies = [{
path: [''],
entity: schema
}];
const delegate = {
INVALID,
getIndex(...path) {
const entity = base.getIndex(...path);
dependencies.push({
path,
entity
});
return base.getIndexEnd(entity, path[2]);
},
getEntity(...path) {
const entity = base.getEntity(...path);
dependencies.push({
path,
entity
});
return entity;
},
getEntities(key) {
const entity = base.getEntitiesObject(key);
dependencies.push({
path: [key],
entity
});
return base.getEntities(key);
}
};
return [delegate, dependencies];
}
}
const getDependency = delegate => path => delegate[['', 'getEntitiesObject', 'getEntity', 'getIndex'][path.length]](...path);
/** Basic POJO state interfaces for normalize side
* Used directly as QueryDelegate, and inherited by NormalizeDelegate
*/
class POJODelegate extends BaseDelegate {
constructor(state) {
super(state);
}
// we must expose the entities object to track in our WeakDependencyMap
// however, this should not be part of the public API
getEntitiesObject(key) {
return this.entities[key];
}
getEntities(key) {
const entities = this.entities[key];
if (entities === undefined) return undefined;
return {
keys() {
return Object.keys(entities);
},
entries() {
return Object.entries(entities);
}
};
}
getEntity(key, pk) {
var _this$entities$key;
return (_this$entities$key = this.entities[key]) == null ? void 0 : _this$entities$key[pk];
}
// this is different return value than QuerySnapshot
getIndex(key, field) {
var _this$indexes$key;
return (_this$indexes$key = this.indexes[key]) == null ? void 0 : _this$indexes$key[field];
}
getIndexEnd(entity, value) {
return entity == null ? void 0 : entity[value];
}
}
/** Handles POJO state for MemoCache methods */
const MemoPolicy = {
QueryDelegate: POJODelegate,
getEntities(entities) {
return ({
key,
pk
}) => {
var _entities$key;
return (_entities$key = entities[key]) == null ? void 0 : _entities$key[pk];
};
}
};
function denormalize(schema, input, entities, args = []) {
// undefined means don't do anything
if (schema === undefined || input === undefined) {
return input;
}
return getUnvisit(MemoPolicy.getEntities(entities), new LocalCache(), args)(schema, input).data;
}
/** Maps a (ordered) list of dependencies to a value.
*
* Useful as a memoization cache for flat/normalized stores.
*
* All dependencies are only weakly referenced, allowing automatic garbage collection
* when any dependencies are no longer used.
*/
class WeakDependencyMap {
next = new WeakMap();
nextPath = undefined;
get(entity, getDependency) {
let curLink = this.next.get(entity);
if (!curLink) return EMPTY;
while (curLink.nextPath) {
var _getDependency;
// we cannot perform lookups with `undefined`, so we use a special object to represent undefined
const nextDependency = (_getDependency = getDependency(curLink.nextPath)) != null ? _getDependency : UNDEF;
curLink = curLink.next.get(nextDependency);
if (!curLink) return EMPTY;
}
// curLink exists, but has no path - so must have a value
return [curLink.value, curLink.journey];
}
set(dependencies, value) {
if (dependencies.length < 1) throw new KeySize();
let curLink = this;
for (const {
path,
entity
} of dependencies) {
let nextLink = curLink.next.get(entity);
if (!nextLink) {
nextLink = new Link();
// void members are represented as a symbol so we can lookup
curLink.next.set(entity != null ? entity : UNDEF, nextLink);
}
curLink.nextPath = path;
curLink = nextLink;
}
// in case there used to be more
curLink.nextPath = undefined;
curLink.value = value;
// we could recompute this on get, but it would have a cost and we optimize for `get`
curLink.journey = dependencies.map(dep => dep.path);
}
}
const EMPTY = [undefined, undefined];
/** Link in a chain */
class Link {
next = new WeakMap();
nextPath = undefined;
value = undefined;
journey = [];
}
class KeySize extends Error {
message = 'Keys must include at least one member';
}
const getVisit = delegate => {
const visit = (schema, value, parent, key, args) => {
if (!value || !schema) {
return value;
}
if (schema.normalize && typeof schema.normalize === 'function') {
if (typeof value !== 'object') {
if (schema.pk) return `${value}`;
return value;
}
return schema.normalize(value, parent, key, args, visit, delegate);
}
if (typeof value !== 'object' || typeof schema !== 'object') return value;
const method = Array.isArray(schema) ? normalize$2 : normalize$1;
return method(schema, value, parent, key, args, visit);
};
return visit;
};
function getCheckLoop() {
const visitedEntities = new Map();
/* Returns true if a circular reference is found */
return function checkLoop(entityKey, pk, input) {
let entitiesOneType = visitedEntities.get(entityKey);
if (!entitiesOneType) {
entitiesOneType = new Map();
visitedEntities.set(entityKey, entitiesOneType);
}
let visitedEntitySet = entitiesOneType.get(pk);
if (!visitedEntitySet) {
visitedEntitySet = new Set();
entitiesOneType.set(pk, visitedEntitySet);
}
if (visitedEntitySet.has(input)) {
return true;
}
visitedEntitySet.add(input);
return false;
};
}
/** Full normalize() logic for POJO state */
class NormalizeDelegate extends POJODelegate {
newEntities = new Map();
newIndexes = new Map();
constructor(state, actionMeta) {
super(state);
this.entitiesMeta = state.entitiesMeta;
this.meta = actionMeta;
this.checkLoop = getCheckLoop();
}
getNewEntity(key, pk) {
return this.getNewEntities(key).get(pk);
}
getNewEntities(key) {
// first time we come across this type of entity
if (!this.newEntities.has(key)) {
this.newEntities.set(key, new Map());
// we will be editing these, so we need to clone them first
this.entities[key] = {
...this.entities[key]
};
this.entitiesMeta[key] = {
...this.entitiesMeta[key]
};
}
return this.newEntities.get(key);
}
getNewIndexes(key) {
if (!this.newIndexes.has(key)) {
this.newIndexes.set(key, new Map());
this.indexes[key] = {
...this.indexes[key]
};
}
return this.newIndexes.get(key);
}
/** Updates an entity using merge lifecycles when it has previously been set */
mergeEntity(schema, pk, incomingEntity) {
const key = schema.key;
// default when this is completely new entity
let nextEntity = incomingEntity;
let nextMeta = this.meta;
// if we already processed this entity during this normalization (in another nested place)
let entity = this.getNewEntity(key, pk);
if (entity) {
nextEntity = schema.merge(entity, incomingEntity);
} else {
// if we find it in the store
entity = this.getEntity(key, pk);
if (entity) {
const meta = this.getMeta(key, pk);
nextEntity = schema.mergeWithStore(meta, nextMeta, entity, incomingEntity);
nextMeta = schema.mergeMetaWithStore(meta, nextMeta, entity, incomingEntity);
}
}
// once we have computed the merged values, set them
this.setEntity(schema, pk, nextEntity, nextMeta);
}
/** Sets an entity overwriting any previously set values */
setEntity(schema, pk, entity, meta = this.meta) {
const key = schema.key;
const newEntities = this.getNewEntities(key);
const updateMeta = !newEntities.has(pk);
newEntities.set(pk, entity);
// update index
if (schema.indexes) {
handleIndexes(pk, schema.indexes, this.getNewIndexes(key), this.indexes[key], entity, this.entities[key]);
}
// set this after index updates so we know what indexes to remove from
this._setEntity(key, pk, entity);
if (updateMeta) this._setMeta(key, pk, meta);
}
/** Invalidates an entity, potentially triggering suspense */
invalidate({
key
}, pk) {
// set directly: any queued updates are meaningless with delete
this.setEntity({
key
}, pk, INVALID);
}
_setEntity(key, pk, entity) {
this.entities[key][pk] = entity;
}
_setMeta(key, pk, meta) {
this.entitiesMeta[key][pk] = meta;
}
getMeta(key, pk) {
return this.entitiesMeta[key][pk];
}
}
function handleIndexes(id, schemaIndexes, indexes, storeIndexes, entity, storeEntities) {
for (const index of schemaIndexes) {
if (!indexes.has(index)) {
indexes.set(index, storeIndexes[index] = {});
}
const indexMap = indexes.get(index);
if (storeEntities[id]) {
delete indexMap[storeEntities[id][index]];
}
// entity already in cache but the index changed
if (storeEntities && storeEntities[id] && storeEntities[id][index] !== entity[index]) {
indexMap[storeEntities[id][index]] = INVALID;
}
if (index in entity) {
indexMap[entity[index]] = id;
} /* istanbul ignore next */else if (process.env.NODE_ENV !== 'production') {
console.warn(`Index not found in entity. Indexes must be top-level members of your entity.
Index: ${index}
Entity: ${JSON.stringify(entity, undefined, 2)}`);
}
}
}
const normalize = (schema, input, args = [], {
entities,
indexes,
entitiesMeta
} = emptyStore, meta = {
fetchedAt: 0,
date: Date.now(),
expiresAt: Infinity
}) => {
// no schema means we don't process at all
if (schema === undefined || schema === null) return {
result: input,
entities,
indexes,
entitiesMeta
};
const schemaType = expectedSchemaType(schema);
if (input === null || typeof input !== schemaType &&
// we will allow a Invalidate schema to be a string or object
!(schema.key !== undefined && schema.pk === undefined && typeof input === 'string')) {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const parseWorks = input => {
try {
return typeof JSON.parse(input) !== 'string';
} catch (e) {
return false;
}
};
if (typeof input === 'string' && parseWorks(input)) {
throw new Error(`Normalizing a string, but this does match schema.
Parsing this input string as JSON worked. This likely indicates fetch function did not parse
the JSON. By default, this only happens if "content-type" header includes "json".
See https://dataclient.io/rest/api/RestEndpoint#parseResponse for more information
Schema: ${JSON.stringify(schema, undefined, 2)}
Input: "${input}"`);
} else {
throw new Error(`Unexpected input given to normalize. Expected type to be "${schemaType}", found "${input === null ? 'null' : typeof input}".
Schema: ${JSON.stringify(schema, undefined, 2)}
Input: "${input}"`);
}
} else {
throw new Error(`Unexpected input given to normalize. Expected type to be "${schemaType}", found "${input === null ? 'null' : typeof input}".`);
}
}
const ret = {
result: '',
entities: {
...entities
},
indexes: {
...indexes
},
entitiesMeta: {
...entitiesMeta
}
};
const visit = getVisit(new NormalizeDelegate(ret, meta));
ret.result = visit(schema, input, input, undefined, args);
return ret;
};
function expectedSchemaType(schema) {
return ['object', 'function'].includes(typeof schema) ? 'object' : typeof schema;
}
const emptyStore = {
entities: {},
indexes: {},
entitiesMeta: {}
};
class GlobalCache {
dependencies = [];
cycleCache = new Map();
cycleIndex = -1;
localCache = new Map();
constructor(getEntity, getCache, resultCache) {
this._getEntity = getEntity;
this._getCache = getCache;
this._resultCache = resultCache;
}
getEntity(pk, schema, entity, computeValue) {
const key = schema.key;
const {
localCacheKey,
cycleCacheKey
} = this.getCacheKey(key);
if (!localCacheKey.get(pk)) {
const globalCache = this._getCache(pk, schema);
const [cacheValue, cachePath] = globalCache.get(entity, this._getEntity);
// TODO: what if this just returned the deps - then we don't need to store them
if (cachePath) {
localCacheKey.set(pk, cacheValue.value);
// TODO: can we store the cache values instead of tracking *all* their sources?
// this is only used for setting endpoints cache correctly. if we got this far we will def need to set as we would have already tried getting it
this.dependencies.push(...cacheValue.dependencies);
return cacheValue.value;
}
// if we don't find in denormalize cache then do full denormalize
else {
const trackingIndex = this.dependencies.length;
cycleCacheKey.set(pk, trackingIndex);
this.dependencies.push({
path: {
key,
pk
},
entity
});
/** NON-GLOBAL_CACHE CODE */
computeValue(localCacheKey);
/** /END NON-GLOBAL_CACHE CODE */
cycleCacheKey.delete(pk);
// if in cycle, use the start of the cycle to track all deps
// otherwise, we use our own trackingIndex
const localKey = this.dependencies.slice(this.cycleIndex === -1 ? trackingIndex : this.cycleIndex);
const cacheValue = {
dependencies: localKey,
value: localCacheKey.get(pk)
};
globalCache.set(localKey, cacheValue);
// start of cycle - reset cycle detection
if (this.cycleIndex === trackingIndex) {
this.cycleIndex = -1;
}
}
} else {
// cycle detected
if (cycleCacheKey.has(pk)) {
this.cycleIndex = cycleCacheKey.get(pk);
} else {
// with no cycle, globalCacheEntry will have already been set
this.dependencies.push({
path: {
key,
pk
},
entity
});
}
}
return localCacheKey.get(pk);
}
getCacheKey(key) {
if (!this.localCache.has(key)) {
this.localCache.set(key, new Map());
}
if (!this.cycleCache.has(key)) {
this.cycleCache.set(key, new Map());
}
const localCacheKey = this.localCache.get(key);
const cycleCacheKey = this.cycleCache.get(key);
return {
localCacheKey,
cycleCacheKey
};
}
/** Cache varies based on input (=== aka reference) */
getResults(input, cachable, computeValue) {
if (!cachable) {
return {
data: computeValue(),
paths: this.paths()
};
}
let [data, paths] = this._resultCache.get(input, this._getEntity);
if (paths === undefined) {
data = computeValue();
// we want to do this before we add our 'input' entry
paths = this.paths();
// for the first entry, `path` is ignored so empty members is fine
this.dependencies.unshift({
path: {
key: '',
pk: ''
},
entity: input
});
this._resultCache.set(this.dependencies, data);
} else {
paths.shift();
}
return {
data,
paths
};
}
paths() {
return this.dependencies.map(dep => dep.path);
}
}
/**
* Build the result parameter to denormalize from schema alone.
* Tries to compute the entity ids from params.
*/
function buildQueryKey(delegate) {
return function queryKey$2(schema, args) {
// schema classes
if (canQuery(schema)) {
return schema.queryKey(args, queryKey$2, delegate);
}
// plain case
if (typeof schema === 'object' && schema) {
const method = Array.isArray(schema) ? queryKey$1 : queryKey;
return method(schema, args, queryKey$2, delegate);
}
// fallback for things like null or undefined
return schema;
};
}
function canQuery(schema) {
return !!schema && typeof schema.queryKey === 'function';
}
// this only works if entity does a lookup first to see if its entity is 'found'
function validateQueryKey(queryKey) {
if (queryKey === undefined) return false;
if (queryKey && typeof queryKey === 'object' && !Array.isArray(queryKey)) {
return Object.values(queryKey).every(validateQueryKey);
}
return true;
}
const getEntityCaches = entityCache => {
return (pk, schema) => {
var _ref;
const key = schema.key;
// collections should use the entities they collect over
// TODO: this should be based on a public interface
const entityInstance = (_ref = schema.cacheWith) != null ? _ref : schema;
if (!entityCache.has(key)) {
entityCache.set(key, new Map());
}
const entityCacheKey = entityCache.get(key);
if (!entityCacheKey.has(pk)) entityCacheKey.set(pk, new WeakMap());
const entityCachePk = entityCacheKey.get(pk);
let wem = entityCachePk.get(entityInstance);
if (!wem) {
wem = new WeakDependencyMap();
entityCachePk.set(entityInstance, wem);
}
return wem;
};
};
// TODO: make MemoCache generic on the arguments sent to Delegate constructor
/** Singleton to store the memoization cache for denormalization methods */
class MemoCache {
/** Cache for every entity based on its dependencies and its own input */
/** Caches the final denormalized form based on input, entities */
endpoints = new WeakDependencyMap();
/** Caches the queryKey based on schema, args, and any used entities or indexes */
queryKeys = new Map();
constructor(policy = MemoPolicy) {
this.policy = policy;
this._getCache = getEntityCaches(new Map());
}
/** Compute denormalized form maintaining referential equality for same inputs */
denormalize(schema, input, entities, args = []) {
// we already vary based on input, so we don't need endpointKey? TODO: verify
// if (!this.endpoints[endpointKey])
// this.endpoints[endpointKey] = new WeakDependencyMap<EntityPath>();
// undefined means don't do anything
if (schema === undefined) {
return {
data: input,
paths: []
};
}
if (input === undefined) {
return {
data: undefined,
paths: []
};
}
const getEntity = this.policy.getEntities(entities);
return getUnvisit(getEntity, new GlobalCache(getEntity, this._getCache, this.endpoints), args)(schema, input);
}
/** Compute denormalized form maintaining referential equality for same inputs */
query(schema, args, state,
// NOTE: different orders can result in cache busting here; but since it's just a perf penalty we will allow for now
argsKey = JSON.stringify(args)) {
const input = this.buildQueryKey(schema, args, state, argsKey);
if (!input) {
return {
data: undefined,
paths: []
};
}
return this.denormalize(schema, input, state.entities, args);
}
buildQueryKey(schema, args, state,
// NOTE: different orders can result in cache busting here; but since it's just a perf penalty we will allow for now
argsKey = JSON.stringify(args)) {
// This is redundant for buildQueryKey checks, but that was is used for recursion so we still need the checks there
// TODO: If we make each recursive call include cache lookups, we combine these checks together
// null is object so we need double check
if (typeof schema !== 'object' && typeof schema.queryKey !== 'function' || !schema) return schema;
// cache lookup: argsKey -> schema -> ...touched indexes or entities
let queryCache = this.queryKeys.get(argsKey);
if (!queryCache) {
queryCache = new WeakDependencyMap();
this.queryKeys.set(argsKey, queryCache);
}
const baseDelegate = new this.policy.QueryDelegate(state);
// eslint-disable-next-line prefer-const
let [value, paths] = queryCache.get(schema, getDependency(baseDelegate));
// paths undefined is the only way to truly tell nothing was found (the value could have actually been undefined)
if (!paths) {
const [delegate, dependencies] = baseDelegate.tracked(schema);
value = buildQueryKey(delegate)(schema, args);
queryCache.set(dependencies, value);
}
return value;
}
}
var ExpiryStatus = {
Invalid: 1,
InvalidIfStale: 2,
Valid: 3
};
// looser version to allow for cross-package version compatibility
exports.BaseDelegate = BaseDelegate;
exports.ExpiryStatus = ExpiryStatus;
exports.INVALID = INVALID;
exports.MemoCache = MemoCache;
exports.MemoPolicy = MemoPolicy;
exports.WeakDependencyMap = WeakDependencyMap;
exports.denormalize = denormalize;
exports.isEntity = isEntity;
exports.normalize = normalize;
exports.validateQueryKey = validateQueryKey;