mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa44971d81 |
No files matched your search
@@ -143,12 +143,4 @@ export default class GenericLocalMemoryCacher<TFields> implements IEntityGeneric
|
||||
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,80 +1,28 @@
|
||||
import { EntityCacheAdapter, CacheLoadResult, EntityConfiguration, mapKeys } from '@expo/entity';
|
||||
import { EntityConfiguration, PartsCacheAdapter, Parts } from '@expo/entity';
|
||||
import SimplePartsCacher from '@expo/entity/build/SimplePartsCacher';
|
||||
import invariant from 'invariant';
|
||||
|
||||
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
|
||||
|
||||
export default class LocalMemoryCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
private readonly genericLocalMemoryCacher: GenericLocalMemoryCacher<TFields>;
|
||||
|
||||
export default class LocalMemoryCacheAdapter<TFields> extends PartsCacheAdapter<TFields> {
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
localMemoryCache: LocalMemoryCache<TFields>
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
this.genericLocalMemoryCacher = 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))
|
||||
super(
|
||||
entityConfiguration,
|
||||
new SimplePartsCacher(new GenericLocalMemoryCacher(localMemoryCache))
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
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}`);
|
||||
const parts = [
|
||||
return [
|
||||
this.entityConfiguration.tableName,
|
||||
`${this.entityConfiguration.cacheKeyVersion}`,
|
||||
columnName,
|
||||
String(fieldValue),
|
||||
];
|
||||
return this.genericLocalMemoryCacher.makeCacheKey(parts);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -41,7 +41,7 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
const entitySpecificGenericCacher =
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
||||
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
|
||||
)!['genericLocalMemoryCacher'];
|
||||
)!['partsCacher']['cacher'];
|
||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
@@ -153,7 +153,7 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
const entitySpecificGenericCacher =
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
||||
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
|
||||
)!['genericLocalMemoryCacher'];
|
||||
)!['partsCacher']['cacher'];
|
||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
||||
cacheKeyMaker('id', entity1.getID()),
|
||||
]);
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
||||
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
|
||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'test-id-1');
|
||||
expect(localMemoryCache.get(cacheKey)).toMatchObject({
|
||||
id: 'test-id-1',
|
||||
});
|
||||
@@ -89,7 +89,7 @@ describe(LocalMemoryCacheAdapter, () => {
|
||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
|
||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'test-id-1');
|
||||
expect(localMemoryCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EntityCacheAdapter, EntityConfiguration, CacheLoadResult, mapKeys } from '@expo/entity';
|
||||
import { EntityConfiguration, PartsCacheAdapter, Parts } from '@expo/entity';
|
||||
import SimplePartsCacher from '@expo/entity/build/SimplePartsCacher';
|
||||
import invariant from 'invariant';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
@@ -41,85 +42,36 @@ export interface RedisCacheAdapterContext {
|
||||
ttlSecondsNegative: number;
|
||||
}
|
||||
|
||||
export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||
private readonly genericRedisCacher: GenericRedisCacher<TFields>;
|
||||
export default class RedisCacheAdapter<TFields> extends PartsCacheAdapter<TFields> {
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
private readonly context: RedisCacheAdapterContext
|
||||
) {
|
||||
super(entityConfiguration);
|
||||
|
||||
this.genericRedisCacher = new GenericRedisCacher(
|
||||
{
|
||||
redisClient: context.redisClient,
|
||||
ttlSecondsNegative: context.ttlSecondsNegative,
|
||||
ttlSecondsPositive: context.ttlSecondsPositive,
|
||||
},
|
||||
entityConfiguration
|
||||
super(
|
||||
entityConfiguration,
|
||||
new SimplePartsCacher(
|
||||
new GenericRedisCacher(
|
||||
{
|
||||
redisClient: context.redisClient,
|
||||
ttlSecondsNegative: context.ttlSecondsNegative,
|
||||
ttlSecondsPositive: context.ttlSecondsPositive,
|
||||
},
|
||||
entityConfiguration
|
||||
),
|
||||
context.makeKeyFn
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
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.context.makeKeyFn(
|
||||
return [
|
||||
this.context.cacheKeyPrefix,
|
||||
this.entityConfiguration.tableName,
|
||||
`v2.${this.entityConfiguration.cacheKeyVersion}`,
|
||||
columnName,
|
||||
String(fieldValue)
|
||||
);
|
||||
String(fieldValue),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,11 @@ describe(RedisCacheAdapter, () => {
|
||||
ttlSecondsNegative: 2,
|
||||
});
|
||||
|
||||
redisResults.set(cacheAdapter['makeCacheKey']('id', 'wat'), JSON.stringify({ id: 'wat' }));
|
||||
redisResults.set(cacheAdapter['makeCacheKey']('id', 'who'), '');
|
||||
redisResults.set(
|
||||
cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat'),
|
||||
JSON.stringify({ id: 'wat' })
|
||||
);
|
||||
redisResults.set(cacheAdapter['partsCacher']['makeCacheKey']('id', 'who'), '');
|
||||
|
||||
const results = await cacheAdapter.loadManyAsync('id', ['wat', 'who', 'why']);
|
||||
|
||||
@@ -91,7 +94,7 @@ describe(RedisCacheAdapter, () => {
|
||||
});
|
||||
await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat' }]]));
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
|
||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat');
|
||||
expect(redisResults.get(cacheKey)).toMatchObject({
|
||||
value: JSON.stringify({ id: 'wat' }),
|
||||
code: 'EX',
|
||||
@@ -126,7 +129,7 @@ describe(RedisCacheAdapter, () => {
|
||||
});
|
||||
await cacheAdapter.cacheDBMissesAsync('id', ['wat']);
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
|
||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat');
|
||||
expect(redisResults.get(cacheKey)).toMatchObject({
|
||||
value: '',
|
||||
code: 'EX',
|
||||
@@ -150,7 +153,7 @@ describe(RedisCacheAdapter, () => {
|
||||
});
|
||||
await cacheAdapter.invalidateManyAsync('id', ['wat']);
|
||||
|
||||
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
|
||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'wat');
|
||||
verify(mockRedisClient.del(cacheKey)).once();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { EntityConfiguration, GenericSecondaryEntityCache } from '@expo/entity';
|
||||
import {
|
||||
EntityConfiguration,
|
||||
GenericSecondaryEntityCache,
|
||||
Parts,
|
||||
SimplePartsCacher,
|
||||
} from '@expo/entity';
|
||||
import { GenericRedisCacheContext, GenericRedisCacher } from '@expo/entity-cache-adapter-redis';
|
||||
|
||||
/**
|
||||
@@ -13,8 +18,15 @@ export default class RedisSecondaryEntityCache<
|
||||
constructor(
|
||||
entityConfiguration: EntityConfiguration<TFields>,
|
||||
genericRedisCacheContext: GenericRedisCacheContext,
|
||||
constructRedisKey: (params: Readonly<TLoadParams>) => string
|
||||
makeRedisSpecificKey: (...parts: Parts) => string,
|
||||
getParts: (params: Readonly<TLoadParams>) => Parts
|
||||
) {
|
||||
super(new GenericRedisCacher(genericRedisCacheContext, entityConfiguration), constructRedisKey);
|
||||
super(
|
||||
new SimplePartsCacher(
|
||||
new GenericRedisCacher(genericRedisCacheContext, entityConfiguration),
|
||||
makeRedisSpecificKey
|
||||
),
|
||||
getParts
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -87,7 +87,8 @@ describe(RedisSecondaryEntityCache, () => {
|
||||
new RedisSecondaryEntityCache(
|
||||
redisTestEntityConfiguration,
|
||||
redisCacheAdapterContext,
|
||||
(loadParams) => `test-key-${loadParams.id}`
|
||||
(...parts) => parts.join(':redis-specific-delimiter:'),
|
||||
(loadParams) => [`test-key-${loadParams.id}`]
|
||||
),
|
||||
RedisTestEntity.loader(viewerContext)
|
||||
);
|
||||
@@ -126,7 +127,8 @@ describe(RedisSecondaryEntityCache, () => {
|
||||
new RedisSecondaryEntityCache(
|
||||
redisTestEntityConfiguration,
|
||||
redisCacheAdapterContext,
|
||||
(loadParams) => `test-key-${loadParams.id}`
|
||||
(...parts) => parts.join(':redis-specific-delimiter:'),
|
||||
(loadParams) => [`test-key-${loadParams.id}`]
|
||||
),
|
||||
RedisTestEntity.loader(viewerContext)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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 { ISecondaryEntityCache } from './EntitySecondaryCacheLoader';
|
||||
import IEntityGenericCacher from './IEntityGenericCacher';
|
||||
import PartsCacher, { Parts, PartsKey } from './PartsCacher';
|
||||
import { CacheStatus } from './internal/ReadThroughEntityCache';
|
||||
import { filterMap, zipToMap } from './utils/collections/maps';
|
||||
import { filterMap } 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
|
||||
@@ -14,8 +14,8 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
||||
implements ISecondaryEntityCache<TFields, TLoadParams>
|
||||
{
|
||||
constructor(
|
||||
protected readonly cacher: IEntityGenericCacher<TFields>,
|
||||
protected readonly constructCacheKey: (params: Readonly<TLoadParams>) => string
|
||||
protected readonly partsCacher: PartsCacher<TFields>,
|
||||
protected readonly getParts: (params: Readonly<TLoadParams>) => Parts
|
||||
) {}
|
||||
|
||||
public async loadManyThroughAsync(
|
||||
@@ -24,17 +24,22 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
||||
fetcherLoadParamsArray: readonly Readonly<TLoadParams>[]
|
||||
) => Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>>
|
||||
): Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> {
|
||||
const cacheKeys = loadParamsArray.map(this.constructCacheKey);
|
||||
const cacheKeyToLoadParamsMap = zipToMap(cacheKeys, loadParamsArray);
|
||||
const partsList = loadParamsArray.map(this.getParts);
|
||||
const partsKeysToLoadParamsMap = new Map(
|
||||
loadParamsArray.map((loadParams) => {
|
||||
const parts = this.getParts(loadParams);
|
||||
return [PartsCacher.getPartsKey(...parts), loadParams];
|
||||
})
|
||||
);
|
||||
|
||||
const cacheLoadResults = await this.cacher.loadManyAsync(cacheKeys);
|
||||
const cacheLoadResults = await this.partsCacher.loadManyAsync(partsList);
|
||||
|
||||
invariant(
|
||||
cacheLoadResults.size === loadParamsArray.length,
|
||||
`${this.constructor.name} loadMany should return a result for each key`
|
||||
);
|
||||
|
||||
const cacheKeysToFetch = Array.from(
|
||||
const partsKeysToFetch = Array.from(
|
||||
filterMap(
|
||||
cacheLoadResults,
|
||||
(cacheLoadResult) => cacheLoadResult.status === CacheStatus.MISS
|
||||
@@ -43,18 +48,18 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
||||
|
||||
// put cache hits in result map
|
||||
const results: Map<Readonly<TLoadParams>, Readonly<TFields> | null> = new Map();
|
||||
cacheLoadResults.forEach((cacheLoadResult, cacheKey) => {
|
||||
cacheLoadResults.forEach((cacheLoadResult, partsKey) => {
|
||||
if (cacheLoadResult.status === CacheStatus.HIT) {
|
||||
const loadParams = cacheKeyToLoadParamsMap.get(cacheKey);
|
||||
const loadParams = partsKeysToLoadParamsMap.get(partsKey);
|
||||
invariant(loadParams !== undefined, 'load params should be in cache key map');
|
||||
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
|
||||
if (cacheKeysToFetch.length > 0) {
|
||||
const loadParamsToFetch = cacheKeysToFetch.map((cacheKey) => {
|
||||
const loadParams = cacheKeyToLoadParamsMap.get(cacheKey);
|
||||
if (partsKeysToFetch.length > 0) {
|
||||
const loadParamsToFetch = partsKeysToFetch.map((partKey) => {
|
||||
const loadParams = partsKeysToLoadParamsMap.get(partKey);
|
||||
invariant(loadParams !== undefined, 'load params should be in cache key map');
|
||||
return loadParams;
|
||||
});
|
||||
@@ -69,17 +74,20 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
||||
results.set(fetchMiss, null);
|
||||
}
|
||||
|
||||
const objectsToCache: Map<string, Readonly<TFields>> = new Map();
|
||||
const objectsToCache: Map<PartsKey, Readonly<TFields>> = new Map();
|
||||
for (const [loadParams, object] of fetchResults.entries()) {
|
||||
if (object) {
|
||||
objectsToCache.set(this.constructCacheKey(loadParams), object);
|
||||
const parts = this.getParts(loadParams);
|
||||
objectsToCache.set(PartsCacher.getPartsKey(...parts), object);
|
||||
results.set(loadParams, object);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.cacher.cacheManyAsync(objectsToCache),
|
||||
this.cacher.cacheDBMissesAsync(fetchMisses.map(this.constructCacheKey)),
|
||||
this.partsCacher.cacheManyAsync(objectsToCache),
|
||||
this.partsCacher.cacheDBMissesAsync(
|
||||
fetchMisses.map((loadParams) => this.getParts(loadParams))
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -92,7 +100,7 @@ export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
||||
* @param loadParamsArray - load params to invalidate
|
||||
*/
|
||||
public invalidateManyAsync(loadParamsArray: readonly Readonly<TLoadParams>[]): Promise<void> {
|
||||
const cacheKeys = loadParamsArray.map(this.constructCacheKey);
|
||||
return this.cacher.invalidateManyAsync(cacheKeys);
|
||||
const partsList = loadParamsArray.map(this.getParts);
|
||||
return this.partsCacher.invalidateManyAsync(partsList);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ export { default as IEntityCacheAdapterProvider } from './IEntityCacheAdapterPro
|
||||
export { default as IEntityDatabaseAdapterProvider } from './IEntityDatabaseAdapterProvider';
|
||||
export { default as EntityQueryContextProvider } from './EntityQueryContextProvider';
|
||||
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 ViewerContext } from './ViewerContext';
|
||||
export { default as ViewerScopedEntityCompanion } from './ViewerScopedEntityCompanion';
|
||||
|
||||
Reference in new issue
Block a user