Compare commits

..
Author SHA1 Message Date
Quinlan Jung b61341057b [cache-adapter-local-memory] export classes 2022-03-09 17:53:23 -08:00
18 changed files with 174 additions and 386 deletions

No files matched your search

@@ -143,4 +143,12 @@ export default class GenericLocalMemoryCacher<TFields> implements IEntityGeneric
this.localMemoryCache.del(key); this.localMemoryCache.del(key);
} }
} }
public makeCacheKey(parts: string[]): string {
const delimiter = ':';
const escapedParts = parts.map((part) =>
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
);
return escapedParts.join(delimiter);
}
} }
@@ -1,28 +1,80 @@
import { EntityConfiguration, PartsCacheAdapter, Parts } from '@expo/entity'; import { EntityCacheAdapter, CacheLoadResult, EntityConfiguration, mapKeys } from '@expo/entity';
import SimplePartsCacher from '@expo/entity/build/SimplePartsCacher';
import invariant from 'invariant'; import invariant from 'invariant';
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher'; import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
export default class LocalMemoryCacheAdapter<TFields> extends PartsCacheAdapter<TFields> { export default class LocalMemoryCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
private readonly genericLocalMemoryCacher: GenericLocalMemoryCacher<TFields>;
constructor( constructor(
entityConfiguration: EntityConfiguration<TFields>, entityConfiguration: EntityConfiguration<TFields>,
localMemoryCache: LocalMemoryCache<TFields> localMemoryCache: LocalMemoryCache<TFields>
) { ) {
super( super(entityConfiguration);
entityConfiguration, this.genericLocalMemoryCacher = new GenericLocalMemoryCacher(localMemoryCache);
new SimplePartsCacher(new GenericLocalMemoryCacher(localMemoryCache)) }
public async loadManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
const localMemoryCacheKeyToFieldValueMapping = new Map(
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
);
const cacheResults = await this.genericLocalMemoryCacher.loadManyAsync(
Array.from(localMemoryCacheKeyToFieldValueMapping.keys())
);
return mapKeys(cacheResults, (cacheKey) => {
const fieldValue = localMemoryCacheKeyToFieldValueMapping.get(cacheKey);
invariant(
fieldValue !== undefined,
'Unspecified cache key %s returned from generic local memory cacher',
cacheKey
);
return fieldValue;
});
}
public async cacheManyAsync<N extends keyof TFields>(
fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void> {
await this.genericLocalMemoryCacher.cacheManyAsync(
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
); );
} }
getParts<N extends keyof TFields>(fieldName: N, fieldValue: NonNullable<TFields[N]>): Parts { public async cacheDBMissesAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.genericLocalMemoryCacher.cacheDBMissesAsync(
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
);
}
public async invalidateManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.genericLocalMemoryCacher.invalidateManyAsync(
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
);
}
private makeCacheKey<N extends keyof TFields>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): string {
const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName); const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
invariant(columnName, `database field mapping missing for ${fieldName}`); invariant(columnName, `database field mapping missing for ${fieldName}`);
return [ const parts = [
this.entityConfiguration.tableName, this.entityConfiguration.tableName,
`${this.entityConfiguration.cacheKeyVersion}`, `${this.entityConfiguration.cacheKeyVersion}`,
columnName, columnName,
String(fieldValue), String(fieldValue),
]; ];
return this.genericLocalMemoryCacher.makeCacheKey(parts);
} }
} }
@@ -8,7 +8,7 @@ import {
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher'; import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
import LocalMemoryCacheAdapter from './LocalMemoryCacheAdapter'; import LocalMemoryCacheAdapter from './LocalMemoryCacheAdapter';
export class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider { export default class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
// local memory cache adapters should be shared/reused across requests // local memory cache adapters should be shared/reused across requests
static localMemoryCacheAdapterMap = new Map<string, LocalMemoryCacheAdapter<any>>(); static localMemoryCacheAdapterMap = new Map<string, LocalMemoryCacheAdapter<any>>();
@@ -41,7 +41,7 @@ describe(LocalMemoryCacheAdapter, () => {
const entitySpecificGenericCacher = const entitySpecificGenericCacher =
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get( LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
)!['partsCacher']['cacher']; )!['genericLocalMemoryCacher'];
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([ const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
cacheKeyMaker('id', entity1.getID()), cacheKeyMaker('id', entity1.getID()),
]); ]);
@@ -153,7 +153,7 @@ describe(LocalMemoryCacheAdapter, () => {
const entitySpecificGenericCacher = const entitySpecificGenericCacher =
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get( LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
)!['partsCacher']['cacher']; )!['genericLocalMemoryCacher'];
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([ const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
cacheKeyMaker('id', entity1.getID()), cacheKeyMaker('id', entity1.getID()),
]); ]);
@@ -72,7 +72,7 @@ describe(LocalMemoryCacheAdapter, () => {
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache); const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]])); await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'test-id-1'); const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
expect(localMemoryCache.get(cacheKey)).toMatchObject({ expect(localMemoryCache.get(cacheKey)).toMatchObject({
id: 'test-id-1', id: 'test-id-1',
}); });
@@ -89,7 +89,7 @@ describe(LocalMemoryCacheAdapter, () => {
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache); const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']); await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'test-id-1'); const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
expect(localMemoryCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE); expect(localMemoryCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
}); });
}); });
@@ -0,0 +1,10 @@
/* eslint-disable tsdoc/syntax */
/**
* @packageDocumentation
* @module @expo/entity-cache-adapter-local-memory
*/
export { default as LocalMemoryCacheAdapter } from './LocalMemoryCacheAdapter';
export { default as LocalMemoryCacheAdapterProvider } from './LocalMemoryCacheAdapterProvider';
export { default as GenericLocalMemoryCacher } from './GenericLocalMemoryCacher';
export * from './GenericLocalMemoryCacher';
@@ -1,5 +1,4 @@
import { EntityConfiguration, PartsCacheAdapter, Parts } from '@expo/entity'; import { EntityCacheAdapter, EntityConfiguration, CacheLoadResult, mapKeys } from '@expo/entity';
import SimplePartsCacher from '@expo/entity/build/SimplePartsCacher';
import invariant from 'invariant'; import invariant from 'invariant';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
@@ -42,36 +41,85 @@ export interface RedisCacheAdapterContext {
ttlSecondsNegative: number; ttlSecondsNegative: number;
} }
export default class RedisCacheAdapter<TFields> extends PartsCacheAdapter<TFields> { export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
private readonly genericRedisCacher: GenericRedisCacher<TFields>;
constructor( constructor(
entityConfiguration: EntityConfiguration<TFields>, entityConfiguration: EntityConfiguration<TFields>,
private readonly context: RedisCacheAdapterContext private readonly context: RedisCacheAdapterContext
) { ) {
super( super(entityConfiguration);
entityConfiguration,
new SimplePartsCacher( this.genericRedisCacher = new GenericRedisCacher(
new GenericRedisCacher(
{ {
redisClient: context.redisClient, redisClient: context.redisClient,
ttlSecondsNegative: context.ttlSecondsNegative, ttlSecondsNegative: context.ttlSecondsNegative,
ttlSecondsPositive: context.ttlSecondsPositive, ttlSecondsPositive: context.ttlSecondsPositive,
}, },
entityConfiguration entityConfiguration
),
context.makeKeyFn
)
); );
} }
getParts<N extends keyof TFields>(fieldName: N, fieldValue: NonNullable<TFields[N]>): Parts { public async loadManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
const redisCacheKeyToFieldValueMapping = new Map(
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
);
const cacheResults = await this.genericRedisCacher.loadManyAsync(
Array.from(redisCacheKeyToFieldValueMapping.keys())
);
return mapKeys(cacheResults, (redisCacheKey) => {
const fieldValue = redisCacheKeyToFieldValueMapping.get(redisCacheKey);
invariant(
fieldValue !== undefined,
'Unspecified cache key %s returned from generic Redis cacher',
redisCacheKey
);
return fieldValue;
});
}
public async cacheManyAsync<N extends keyof TFields>(
fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void> {
await this.genericRedisCacher.cacheManyAsync(
mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
);
}
public async cacheDBMissesAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.genericRedisCacher.cacheDBMissesAsync(
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
);
}
public async invalidateManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.genericRedisCacher.invalidateManyAsync(
fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
);
}
private makeCacheKey<N extends keyof TFields>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): string {
const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName); const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
invariant(columnName, `database field mapping missing for ${fieldName}`); invariant(columnName, `database field mapping missing for ${fieldName}`);
return [ return this.context.makeKeyFn(
this.context.cacheKeyPrefix, this.context.cacheKeyPrefix,
this.entityConfiguration.tableName, this.entityConfiguration.tableName,
`v2.${this.entityConfiguration.cacheKeyVersion}`, `v2.${this.entityConfiguration.cacheKeyVersion}`,
columnName, columnName,
String(fieldValue), String(fieldValue)
]; );
} }
} }
@@ -39,11 +39,8 @@ describe(RedisCacheAdapter, () => {
ttlSecondsNegative: 2, ttlSecondsNegative: 2,
}); });
redisResults.set( redisResults.set(cacheAdapter['makeCacheKey']('id', 'wat'), JSON.stringify({ id: 'wat' }));
cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat'), redisResults.set(cacheAdapter['makeCacheKey']('id', 'who'), '');
JSON.stringify({ id: 'wat' })
);
redisResults.set(cacheAdapter['partsCacher']['makeCacheKey']('id', 'who'), '');
const results = await cacheAdapter.loadManyAsync('id', ['wat', 'who', 'why']); const results = await cacheAdapter.loadManyAsync('id', ['wat', 'who', 'why']);
@@ -94,7 +91,7 @@ describe(RedisCacheAdapter, () => {
}); });
await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat' }]])); await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat' }]]));
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat'); const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
expect(redisResults.get(cacheKey)).toMatchObject({ expect(redisResults.get(cacheKey)).toMatchObject({
value: JSON.stringify({ id: 'wat' }), value: JSON.stringify({ id: 'wat' }),
code: 'EX', code: 'EX',
@@ -129,7 +126,7 @@ describe(RedisCacheAdapter, () => {
}); });
await cacheAdapter.cacheDBMissesAsync('id', ['wat']); await cacheAdapter.cacheDBMissesAsync('id', ['wat']);
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat'); const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
expect(redisResults.get(cacheKey)).toMatchObject({ expect(redisResults.get(cacheKey)).toMatchObject({
value: '', value: '',
code: 'EX', code: 'EX',
@@ -153,7 +150,7 @@ describe(RedisCacheAdapter, () => {
}); });
await cacheAdapter.invalidateManyAsync('id', ['wat']); await cacheAdapter.invalidateManyAsync('id', ['wat']);
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat'); const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
verify(mockRedisClient.del(cacheKey)).once(); verify(mockRedisClient.del(cacheKey)).once();
}); });
@@ -1,9 +1,4 @@
import { import { EntityConfiguration, GenericSecondaryEntityCache } from '@expo/entity';
EntityConfiguration,
GenericSecondaryEntityCache,
Parts,
SimplePartsCacher,
} from '@expo/entity';
import { GenericRedisCacheContext, GenericRedisCacher } from '@expo/entity-cache-adapter-redis'; import { GenericRedisCacheContext, GenericRedisCacher } from '@expo/entity-cache-adapter-redis';
/** /**
@@ -18,15 +13,8 @@ export default class RedisSecondaryEntityCache<
constructor( constructor(
entityConfiguration: EntityConfiguration<TFields>, entityConfiguration: EntityConfiguration<TFields>,
genericRedisCacheContext: GenericRedisCacheContext, genericRedisCacheContext: GenericRedisCacheContext,
makeRedisSpecificKey: (...parts: Parts) => string, constructRedisKey: (params: Readonly<TLoadParams>) => string
getParts: (params: Readonly<TLoadParams>) => Parts
) { ) {
super( super(new GenericRedisCacher(genericRedisCacheContext, entityConfiguration), constructRedisKey);
new SimplePartsCacher(
new GenericRedisCacher(genericRedisCacheContext, entityConfiguration),
makeRedisSpecificKey
),
getParts
);
} }
} }
@@ -87,8 +87,7 @@ describe(RedisSecondaryEntityCache, () => {
new RedisSecondaryEntityCache( new RedisSecondaryEntityCache(
redisTestEntityConfiguration, redisTestEntityConfiguration,
redisCacheAdapterContext, redisCacheAdapterContext,
(...parts) => parts.join(':redis-specific-delimiter:'), (loadParams) => `test-key-${loadParams.id}`
(loadParams) => [`test-key-${loadParams.id}`]
), ),
RedisTestEntity.loader(viewerContext) RedisTestEntity.loader(viewerContext)
); );
@@ -127,8 +126,7 @@ describe(RedisSecondaryEntityCache, () => {
new RedisSecondaryEntityCache( new RedisSecondaryEntityCache(
redisTestEntityConfiguration, redisTestEntityConfiguration,
redisCacheAdapterContext, redisCacheAdapterContext,
(...parts) => parts.join(':redis-specific-delimiter:'), (loadParams) => `test-key-${loadParams.id}`
(loadParams) => [`test-key-${loadParams.id}`]
), ),
RedisTestEntity.loader(viewerContext) RedisTestEntity.loader(viewerContext)
); );
@@ -1,82 +0,0 @@
import { CacheLoadResult } from '@expo/entity/src/internal/ReadThroughEntityCache';
import invariant from 'invariant';
import PartsCacher, { Parts, PartsKey } from './PartsCacher';
import { CacheStatus } from './internal/ReadThroughEntityCache';
/**
* A composed cacher by which objects are stored, fetched and removed in a series of caches
* */
export default class ComposedPartsCacher<TFields> extends PartsCacher<TFields> {
constructor(
/**
* The series of cachers to use, in order of evaluation
* For example, [cacheAdapterEvaluatedFirst, cacheAdapterEvaluatedSecond, cacheAdapterEvaluatedThird]
*/
private readonly partsCachers: PartsCacher<TFields>[]
) {
super();
}
public async loadManyAsync(
partsList: readonly Parts[]
): Promise<ReadonlyMap<PartsKey, CacheLoadResult<TFields>>> {
const resultsFromAllCachers = new Map<PartsKey, CacheLoadResult<TFields>>();
const partsKeysToParts = new Map(
partsList.map((parts) => [PartsCacher.getPartsKey(...parts), parts])
);
let partsKeysToLookup: PartsKey[] = partsList.map((parts) => PartsCacher.getPartsKey(...parts));
for (const partsCacher of this.partsCachers) {
const partsListToLookup = partsKeysToLookup.map((partsKey) => {
const parts = partsKeysToParts.get(partsKey);
invariant(parts, 'parts cannot be undefined');
return parts;
});
const cacheResults = await partsCacher.loadManyAsync(partsListToLookup);
const cacheMisses = [];
for (const [partsKey, cacheResult] of cacheResults) {
if (cacheResult.status === CacheStatus.MISS) {
cacheMisses.push(partsKey);
} else {
resultsFromAllCachers.set(partsKey, cacheResult);
}
}
partsKeysToLookup = cacheMisses;
}
// None of the cachers have these values, so they are misses
for (const partsKey of partsKeysToLookup) {
resultsFromAllCachers.set(partsKey, {
status: CacheStatus.MISS,
});
}
return resultsFromAllCachers;
}
public async cacheManyAsync(objectMap: ReadonlyMap<PartsKey, Readonly<TFields>>): Promise<void> {
// perform action on lower layers first
for (let i = this.partsCachers.length - 1; i >= 0; i--) {
const partsCacher = this.partsCachers[i];
invariant(partsCacher, 'parts cacher cannot be undefined');
await partsCacher.cacheManyAsync(objectMap);
}
}
public async cacheDBMissesAsync(partsList: readonly Parts[]): Promise<void> {
// perform action on lower layers first
for (let i = this.partsCachers.length - 1; i >= 0; i--) {
const partsCacher = this.partsCachers[i];
invariant(partsCacher, 'parts cacher cannot be undefined');
await partsCacher.cacheDBMissesAsync(partsList);
}
}
public async invalidateManyAsync(partsList: readonly Parts[]): Promise<void> {
// perform action on lower layers first
for (let i = this.partsCachers.length - 1; i >= 0; i--) {
const cacheAdapter = this.partsCachers[i];
invariant(cacheAdapter, 'parts cacher cannot be undefined');
await cacheAdapter.invalidateManyAsync(partsList);
}
}
}
@@ -1,9 +1,9 @@
import invariant from 'invariant'; import invariant from 'invariant';
import { ISecondaryEntityCache } from './EntitySecondaryCacheLoader'; import { ISecondaryEntityCache } from './EntitySecondaryCacheLoader';
import PartsCacher, { Parts, PartsKey } from './PartsCacher'; import IEntityGenericCacher from './IEntityGenericCacher';
import { CacheStatus } from './internal/ReadThroughEntityCache'; import { CacheStatus } from './internal/ReadThroughEntityCache';
import { filterMap } from './utils/collections/maps'; import { filterMap, zipToMap } from './utils/collections/maps';
/** /**
* A custom secondary read-through entity cache is a way to add a custom second layer of caching for a particular * A custom secondary read-through entity cache is a way to add a custom second layer of caching for a particular
@@ -14,8 +14,8 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
implements ISecondaryEntityCache<TFields, TLoadParams> implements ISecondaryEntityCache<TFields, TLoadParams>
{ {
constructor( constructor(
protected readonly partsCacher: PartsCacher<TFields>, protected readonly cacher: IEntityGenericCacher<TFields>,
protected readonly getParts: (params: Readonly<TLoadParams>) => Parts protected readonly constructCacheKey: (params: Readonly<TLoadParams>) => string
) {} ) {}
public async loadManyThroughAsync( public async loadManyThroughAsync(
@@ -24,22 +24,17 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
fetcherLoadParamsArray: readonly Readonly<TLoadParams>[] fetcherLoadParamsArray: readonly Readonly<TLoadParams>[]
) => Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> ) => Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>>
): Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> { ): Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> {
const partsList = loadParamsArray.map(this.getParts); const cacheKeys = loadParamsArray.map(this.constructCacheKey);
const partsKeysToLoadParamsMap = new Map( const cacheKeyToLoadParamsMap = zipToMap(cacheKeys, loadParamsArray);
loadParamsArray.map((loadParams) => {
const parts = this.getParts(loadParams);
return [PartsCacher.getPartsKey(...parts), loadParams];
})
);
const cacheLoadResults = await this.partsCacher.loadManyAsync(partsList); const cacheLoadResults = await this.cacher.loadManyAsync(cacheKeys);
invariant( invariant(
cacheLoadResults.size === loadParamsArray.length, cacheLoadResults.size === loadParamsArray.length,
`${this.constructor.name} loadMany should return a result for each key` `${this.constructor.name} loadMany should return a result for each key`
); );
const partsKeysToFetch = Array.from( const cacheKeysToFetch = Array.from(
filterMap( filterMap(
cacheLoadResults, cacheLoadResults,
(cacheLoadResult) => cacheLoadResult.status === CacheStatus.MISS (cacheLoadResult) => cacheLoadResult.status === CacheStatus.MISS
@@ -48,18 +43,18 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
// put cache hits in result map // put cache hits in result map
const results: Map<Readonly<TLoadParams>, Readonly<TFields> | null> = new Map(); const results: Map<Readonly<TLoadParams>, Readonly<TFields> | null> = new Map();
cacheLoadResults.forEach((cacheLoadResult, partsKey) => { cacheLoadResults.forEach((cacheLoadResult, cacheKey) => {
if (cacheLoadResult.status === CacheStatus.HIT) { if (cacheLoadResult.status === CacheStatus.HIT) {
const loadParams = partsKeysToLoadParamsMap.get(partsKey); const loadParams = cacheKeyToLoadParamsMap.get(cacheKey);
invariant(loadParams !== undefined, 'load params should be in cache key map'); invariant(loadParams !== undefined, 'load params should be in cache key map');
results.set(loadParams, cacheLoadResult.item); results.set(loadParams, cacheLoadResult.item);
} }
}); });
// fetch any misses from DB, add DB objects to results, cache DB results, inform cache of any missing DB results // fetch any misses from DB, add DB objects to results, cache DB results, inform cache of any missing DB results
if (partsKeysToFetch.length > 0) { if (cacheKeysToFetch.length > 0) {
const loadParamsToFetch = partsKeysToFetch.map((partKey) => { const loadParamsToFetch = cacheKeysToFetch.map((cacheKey) => {
const loadParams = partsKeysToLoadParamsMap.get(partKey); const loadParams = cacheKeyToLoadParamsMap.get(cacheKey);
invariant(loadParams !== undefined, 'load params should be in cache key map'); invariant(loadParams !== undefined, 'load params should be in cache key map');
return loadParams; return loadParams;
}); });
@@ -74,20 +69,17 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
results.set(fetchMiss, null); results.set(fetchMiss, null);
} }
const objectsToCache: Map<PartsKey, Readonly<TFields>> = new Map(); const objectsToCache: Map<string, Readonly<TFields>> = new Map();
for (const [loadParams, object] of fetchResults.entries()) { for (const [loadParams, object] of fetchResults.entries()) {
if (object) { if (object) {
const parts = this.getParts(loadParams); objectsToCache.set(this.constructCacheKey(loadParams), object);
objectsToCache.set(PartsCacher.getPartsKey(...parts), object);
results.set(loadParams, object); results.set(loadParams, object);
} }
} }
await Promise.all([ await Promise.all([
this.partsCacher.cacheManyAsync(objectsToCache), this.cacher.cacheManyAsync(objectsToCache),
this.partsCacher.cacheDBMissesAsync( this.cacher.cacheDBMissesAsync(fetchMisses.map(this.constructCacheKey)),
fetchMisses.map((loadParams) => this.getParts(loadParams))
),
]); ]);
} }
@@ -100,7 +92,7 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
* @param loadParamsArray - load params to invalidate * @param loadParamsArray - load params to invalidate
*/ */
public invalidateManyAsync(loadParamsArray: readonly Readonly<TLoadParams>[]): Promise<void> { public invalidateManyAsync(loadParamsArray: readonly Readonly<TLoadParams>[]): Promise<void> {
const partsList = loadParamsArray.map(this.getParts); const cacheKeys = loadParamsArray.map(this.constructCacheKey);
return this.partsCacher.invalidateManyAsync(partsList); return this.cacher.invalidateManyAsync(cacheKeys);
} }
} }
@@ -1,41 +0,0 @@
import { RedisCacheAdapterContext } from '@expo/entity-cache-adapter-redis';
import invariant from 'invariant';
import ComposedPartsCacher from './ComposedPartsCacher';
import EntityConfiguration from './EntityConfiguration';
import PartsCacheAdapter from './PartsCacheAdapter';
import PartsCacher, { Parts } from './PartsCacher';
/**
* TODO: put this in www
*/
export default class LocalMemoryAndRedisPartsCacheAdapter<
TFields
> extends PartsCacheAdapter<TFields> {
constructor(
entityConfiguration: EntityConfiguration<TFields>,
localMemoryPartsCacher: PartsCacher<TFields>,
redisPartsCacher: PartsCacher<TFields>,
private readonly redisCacheAdapterContext: RedisCacheAdapterContext
) {
super(
entityConfiguration,
new ComposedPartsCacher<TFields>([localMemoryPartsCacher, redisPartsCacher])
);
}
/**
* TODO: this seems wrong - look at moving the redis specific bits into the makeCacheSpecificKey of the SimplePartsCacher
*/
getParts<N extends keyof TFields>(fieldName: N, fieldValue: NonNullable<TFields[N]>): Parts {
const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
invariant(columnName, `database field mapping missing for ${fieldName}`);
return [
this.redisCacheAdapterContext.cacheKeyPrefix,
this.entityConfiguration.tableName,
`v2.${this.entityConfiguration.cacheKeyVersion}`,
columnName,
String(fieldValue),
];
}
}
@@ -1,19 +0,0 @@
import ComposedPartsCacher from './ComposedPartsCacher';
import GenericSecondaryEntityCache from './GenericSecondaryEntityCache';
import PartsCacher, { Parts } from './PartsCacher';
/**
* TODO: put this in www
*/
export default class LocalMemoryAndRedisSecondaryEntityCache<
TFields,
TLoadParams
> extends GenericSecondaryEntityCache<TFields, TLoadParams> {
constructor(
localMemoryPartsCacher: PartsCacher<TFields>,
redisPartsCacher: PartsCacher<TFields>,
getParts: (params: Readonly<TLoadParams>) => Parts
) {
super(new ComposedPartsCacher([localMemoryPartsCacher, redisPartsCacher]), getParts);
}
}
-74
View File
@@ -1,74 +0,0 @@
import invariant from 'invariant';
import EntityCacheAdapter from './EntityCacheAdapter';
import EntityConfiguration from './EntityConfiguration';
import PartsCacher, { Parts } from './PartsCacher';
import { CacheLoadResult } from './internal/ReadThroughEntityCache';
import { mapKeys } from './utils/collections/maps';
export default abstract class PartsCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
constructor(
entityConfiguration: EntityConfiguration<TFields>,
private readonly partsCacher: PartsCacher<TFields>
) {
super(entityConfiguration);
}
public async loadManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
const partsList = fieldValues.map((fieldValue) => this.getParts(fieldName, fieldValue));
const partsKeysToFieldValueMapping = new Map(
fieldValues.map((fieldValue) => {
const parts = this.getParts(fieldName, fieldValue);
return [PartsCacher.getPartsKey(...parts), fieldValue];
})
);
const cacheResults = await this.partsCacher.loadManyAsync(partsList);
return mapKeys(cacheResults, (partsKey) => {
const fieldValue = partsKeysToFieldValueMapping.get(partsKey);
invariant(
fieldValue !== undefined,
'Unspecified cache key %s returned from Redis parts cacher',
partsKey
);
return fieldValue;
});
}
public async cacheManyAsync<N extends keyof TFields>(
fieldName: N,
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
): Promise<void> {
const partsKeyToObject = mapKeys(objectMap, (fieldValue) => {
const parts = this.getParts(fieldName, fieldValue);
return PartsCacher.getPartsKey(...parts);
});
await this.partsCacher.cacheManyAsync(partsKeyToObject);
}
public async cacheDBMissesAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.partsCacher.cacheDBMissesAsync(
fieldValues.map((fieldValue) => this.getParts(fieldName, fieldValue))
);
}
public async invalidateManyAsync<N extends keyof TFields>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<void> {
await this.partsCacher.invalidateManyAsync(
fieldValues.map((fieldValue) => this.getParts(fieldName, fieldValue))
);
}
abstract getParts<N extends keyof TFields>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): Parts;
}
-25
View File
@@ -1,25 +0,0 @@
import { CacheLoadResult } from '@expo/entity/src/internal/ReadThroughEntityCache';
export type Parts = string[];
export type PartsKey = string;
export default abstract class PartsCacher<TFields> {
public abstract loadManyAsync(
partsList: readonly Parts[]
): Promise<ReadonlyMap<PartsKey, CacheLoadResult<TFields>>>;
public abstract cacheManyAsync(
objectMap: ReadonlyMap<PartsKey, Readonly<TFields>>
): Promise<void>;
public abstract cacheDBMissesAsync(partsList: readonly Parts[]): Promise<void>;
public abstract invalidateManyAsync(partsList: readonly Parts[]): Promise<void>;
public static getPartsKey(...parts: Parts): PartsKey {
return JSON.stringify(parts);
}
public static getPartsFromKey(partKey: PartsKey): Parts {
return JSON.parse(partKey);
}
}
-59
View File
@@ -1,59 +0,0 @@
import IEntityGenericCacher from '@expo/entity/src/IEntityGenericCacher';
import { CacheLoadResult } from '@expo/entity/src/internal/ReadThroughEntityCache';
import { mapKeys } from '@expo/entity/src/utils/collections/maps';
import invariant from 'invariant';
import PartsCacher, { Parts, PartsKey } from './PartsCacher';
export default class SimplePartsCacher<TFields> extends PartsCacher<TFields> {
constructor(
private readonly cacher: IEntityGenericCacher<TFields>,
private readonly makeCacheSpecificKey: (...parts: Parts) => string = PartsCacher.getPartsKey
) {
super();
}
public async loadManyAsync(
partsList: readonly Parts[]
): Promise<ReadonlyMap<PartsKey, CacheLoadResult<TFields>>> {
const cacheSpecificKeyToPartKeyMapping = new Map(
partsList.map((parts) => [
this.makeCacheSpecificKey(...parts),
PartsCacher.getPartsKey(...parts),
])
);
const cacheResults = await this.cacher.loadManyAsync(
Array.from(cacheSpecificKeyToPartKeyMapping.keys())
);
return mapKeys(cacheResults, (cacheKey) => {
const partKey = cacheSpecificKeyToPartKeyMapping.get(cacheKey);
invariant(
partKey !== undefined,
'Unspecified cache key %s returned from generic local memory cacher',
cacheKey
);
return partKey;
});
}
public async cacheManyAsync(objectMap: ReadonlyMap<PartsKey, Readonly<TFields>>): Promise<void> {
await this.cacher.cacheManyAsync(
mapKeys(objectMap, (partKey) =>
this.makeCacheSpecificKey(...PartsCacher.getPartsFromKey(partKey))
)
);
}
public async cacheDBMissesAsync(partsList: readonly Parts[]): Promise<void> {
await this.cacher.cacheDBMissesAsync(
partsList.map((parts) => this.makeCacheSpecificKey(...parts))
);
}
public async invalidateManyAsync(partsList: readonly Parts[]): Promise<void> {
await this.cacher.invalidateManyAsync(
partsList.map((parts) => this.makeCacheSpecificKey(...parts))
);
}
}
-5
View File
@@ -45,11 +45,6 @@ export { default as IEntityCacheAdapterProvider } from './IEntityCacheAdapterPro
export { default as IEntityDatabaseAdapterProvider } from './IEntityDatabaseAdapterProvider'; export { default as IEntityDatabaseAdapterProvider } from './IEntityDatabaseAdapterProvider';
export { default as EntityQueryContextProvider } from './EntityQueryContextProvider'; export { default as EntityQueryContextProvider } from './EntityQueryContextProvider';
export { default as IEntityGenericCacher } from './IEntityGenericCacher'; export { default as IEntityGenericCacher } from './IEntityGenericCacher';
export { default as SimplePartsCacher } from './SimplePartsCacher';
export { default as PartsCacher } from './PartsCacher';
export * from './PartsCacher';
export { default as PartsCacheAdapter } from './PartsCacheAdapter';
export { default as ComposedPartsCacher } from './ComposedPartsCacher';
export { default as ReadonlyEntity } from './ReadonlyEntity'; export { default as ReadonlyEntity } from './ReadonlyEntity';
export { default as ViewerContext } from './ViewerContext'; export { default as ViewerContext } from './ViewerContext';
export { default as ViewerScopedEntityCompanion } from './ViewerScopedEntityCompanion'; export { default as ViewerScopedEntityCompanion } from './ViewerScopedEntityCompanion';