UNPKG

@data-client/normalizr

Version:

Normalizes and denormalizes JSON according to schema for Redux and Flux applications

142 lines (125 loc) 4.84 kB
import GlobalCache from './globalCache.js'; import WeakDependencyMap from './WeakDependencyMap.js'; import buildQueryKey from '../buildQueryKey.js'; import { GetEntityCache, getEntityCaches } from './entitiesCache.js'; import { MemoPolicy } from './Policy.js'; import { getDependency } from '../delegate/BaseDelegate.js'; import type { INVALID } from '../denormalize/symbol.js'; import getUnvisit from '../denormalize/unvisit.js'; import type { EntityPath, NormalizedIndex, QueryPath, Schema, } from '../interface.js'; import type { DenormalizeNullable, NormalizeNullable } from '../types.js'; import type { IMemoPolicy, EndpointsCache } from './types.js'; // TODO: make MemoCache generic on the arguments sent to Delegate constructor /** Singleton to store the memoization cache for denormalization methods */ export default class MemoCache { /** Cache for every entity based on its dependencies and its own input */ declare protected _getCache: GetEntityCache; /** Caches the final denormalized form based on input, entities */ protected endpoints: EndpointsCache = new WeakDependencyMap<EntityPath>(); /** Caches the queryKey based on schema, args, and any used entities or indexes */ protected queryKeys: Map<string, WeakDependencyMap<QueryPath>> = new Map(); declare protected policy: IMemoPolicy; constructor(policy: IMemoPolicy = MemoPolicy) { this.policy = policy; this._getCache = getEntityCaches(new Map()); } /** Compute denormalized form maintaining referential equality for same inputs */ denormalize<S extends Schema>( schema: S | undefined, input: unknown, entities: any, args: readonly any[] = [], ): { data: DenormalizeNullable<S> | typeof INVALID; paths: EntityPath[]; } { // 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 as any, paths: [] }; } if (input === undefined) { return { data: undefined as any, 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<S extends Schema>( schema: S, args: readonly any[], state: StateInterface, // NOTE: different orders can result in cache busting here; but since it's just a perf penalty we will allow for now argsKey: string = JSON.stringify(args), ): { data: DenormalizeNullable<S> | typeof INVALID; paths: EntityPath[]; } { const input = this.buildQueryKey(schema, args, state, argsKey); if (!input) { return { data: undefined as any, paths: [] }; } return this.denormalize(schema, input, state.entities, args); } buildQueryKey<S extends Schema>( schema: S, args: readonly any[], state: StateInterface, // NOTE: different orders can result in cache busting here; but since it's just a perf penalty we will allow for now argsKey: string = JSON.stringify(args), ): NormalizeNullable<S> { // 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 as any).queryKey !== 'function') || !schema ) return schema as any; // cache lookup: argsKey -> schema -> ...touched indexes or entities let queryCache = this.queryKeys.get(argsKey) as | WeakDependencyMap<QueryPath, object, any> | undefined; if (!queryCache) { queryCache = new WeakDependencyMap<QueryPath>(); this.queryKeys.set(argsKey, queryCache); } const baseDelegate = new this.policy.QueryDelegate(state); // eslint-disable-next-line prefer-const let [value, paths] = queryCache.get( schema as any, 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; } } type StateInterface = { entities: | Record<string, Record<string, any> | undefined> | { getIn(k: string[]): any; }; indexes: | NormalizedIndex | { getIn(k: string[]): any; }; };