mirror of
https://github.com/expo/entity.git
synced 2026-11-13 20:28:06 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a38e77e9af | ||
|
|
9ba1d430e0 |
No files matched your search
@@ -3,30 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add local memory cache package ([#158](https://github.com/expo/entity/issues/158)) ([85d3575](https://github.com/expo/entity/commit/85d35752fa0919a62efb776a762a6829e83ca7af))
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* Refactor cachers to use an abstract class and interface ([#157](https://github.com/expo/entity/pull/157))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
### Breaking changes
|
|
||||||
|
|
||||||
* Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. Affects any classes extending the EntityCacheAdapter and relying on the transformation behavior of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,5 +4,5 @@
|
|||||||
],
|
],
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"version": "0.24.0"
|
"version": "0.22.0"
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/entity": "file:packages/entity",
|
"@expo/entity": "file:packages/entity",
|
||||||
"@expo/results": "^1.0.0",
|
"@expo/results": "^1.0.0",
|
||||||
"@types/lru-cache": "^5.1.1",
|
|
||||||
"@types/invariant": "^2.2.33",
|
"@types/invariant": "^2.2.33",
|
||||||
"@types/ioredis": "^4.26.4",
|
"@types/ioredis": "^4.26.4",
|
||||||
"@types/jest": "^26.0.10",
|
"@types/jest": "^26.0.10",
|
||||||
@@ -44,7 +43,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/entity": "file:packages/entity",
|
"@expo/entity": "file:packages/entity",
|
||||||
"@expo/entity-cache-adapter-local-memory": "file:packages/entity-cache-adapter-local-memory",
|
|
||||||
"@expo/entity-cache-adapter-redis": "file:packages/entity-cache-adapter-redis",
|
"@expo/entity-cache-adapter-redis": "file:packages/entity-cache-adapter-redis",
|
||||||
"@expo/entity-database-adapter-knex": "file:packages/entity-database-adapter-knex",
|
"@expo/entity-database-adapter-knex": "file:packages/entity-database-adapter-knex",
|
||||||
"@expo/entity-secondary-cache-redis": "file:packages/entity-secondary-cache-redis",
|
"@expo/entity-secondary-cache-redis": "file:packages/entity-secondary-cache-redis",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Change Log
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add local memory cache package ([#158](https://github.com/expo/entity/issues/158)) ([85d3575](https://github.com/expo/entity/commit/85d35752fa0919a62efb776a762a6829e83ca7af))
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# `@expo/entity-cache-adapter-local-memory`
|
|
||||||
|
|
||||||
Cross-request [LRU](https://github.com/isaacs/node-lru-cache) cache adapter for `@expo/entity`. Use
|
|
||||||
this cache with caution - it is nonstandard. The cache is shared between requests in the node process.
|
|
||||||
|
|
||||||
## Why NOT use this cache
|
|
||||||
|
|
||||||
Because this is an in-memory cache, cross-box invalidation is not possible. Do not use this cache
|
|
||||||
if you have the following use cases:
|
|
||||||
|
|
||||||
- The objects stored have high mutability
|
|
||||||
- Reading a stale object from the cache is not acceptable in your application
|
|
||||||
- Cross-box invalidation is not possible
|
|
||||||
|
|
||||||
## Typical use cases
|
|
||||||
|
|
||||||
If your application sees many requests fetching the same objects, you can save a trip to your cache
|
|
||||||
cluster and backing datastore by using this in-memory cache. Here are some good use cases:
|
|
||||||
|
|
||||||
- The objects stored are mostly immutable
|
|
||||||
- You have a low TTL setting in your cache
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
During `EntityCompanionProvider` instantiation:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const createDefaultEntityCompanionProvider = (
|
|
||||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
|
|
||||||
): EntityCompanionProvider => {
|
|
||||||
return new EntityCompanionProvider(
|
|
||||||
metricsAdapter,
|
|
||||||
{
|
|
||||||
...
|
|
||||||
},
|
|
||||||
{
|
|
||||||
['local-memory']: {
|
|
||||||
cacheAdapterProvider: new LocalMemoryCacheAdapterProvider.getProvider(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@expo/entity-cache-adapter-local-memory",
|
|
||||||
"version": "0.24.0",
|
|
||||||
"description": "Cross-request local memory cache adapter for @expo/entity",
|
|
||||||
"files": [
|
|
||||||
"build",
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"main": "build/index.js",
|
|
||||||
"types": "build/index.d.ts",
|
|
||||||
"scripts": {
|
|
||||||
"tsc": "tsc",
|
|
||||||
"clean": "rm -rf build coverage coverage-integration",
|
|
||||||
"lint": "eslint src",
|
|
||||||
"lint-fix": "eslint src --fix",
|
|
||||||
"test": "jest --rootDir . --config ../../resources/jest.config.js",
|
|
||||||
"integration": "../../resources/run-with-docker yarn integration-no-setup",
|
|
||||||
"integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir . --runInBand --passWithNoTests",
|
|
||||||
"barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"entity"
|
|
||||||
],
|
|
||||||
"author": "Expo",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@expo/entity": "*"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": "^7.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@expo/entity": "^0.24.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { CacheLoadResult, CacheStatus, IEntityGenericCacher } from '@expo/entity';
|
|
||||||
import LRUCache from 'lru-cache';
|
|
||||||
|
|
||||||
// Sentinel value we store in local memory to negatively cache a database miss.
|
|
||||||
// The sentinel value is distinct from any (positively) cached value.
|
|
||||||
export const DOES_NOT_EXIST_LOCAL_MEMORY_CACHE = Symbol('doesNotExist');
|
|
||||||
type LocalMemoryCacheValue<TFields> = Readonly<TFields> | typeof DOES_NOT_EXIST_LOCAL_MEMORY_CACHE;
|
|
||||||
export type LocalMemoryCache<TFields> = LRUCache<string, LocalMemoryCacheValue<TFields>>;
|
|
||||||
|
|
||||||
type LRUCacheOptionsV7<K, V> = {
|
|
||||||
/**
|
|
||||||
* the number of most recently used items to keep.
|
|
||||||
* note that we may store fewer items than this if maxSize is hit.
|
|
||||||
*/
|
|
||||||
max: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if you wish to track item size, you must provide a maxSize
|
|
||||||
* note that we still will only keep up to max *actual items*,
|
|
||||||
* so size tracking may cause fewer than max items to be stored.
|
|
||||||
* At the extreme, a single item of maxSize size will cause everything
|
|
||||||
* else in the cache to be dropped when it is added. Use with caution!
|
|
||||||
* Note also that size tracking can negatively impact performance,
|
|
||||||
* though for most cases, only minimally.
|
|
||||||
*/
|
|
||||||
|
|
||||||
maxSize?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function to calculate size of items. useful if storing strings or
|
|
||||||
* buffers or other items where memory size depends on the object itself.
|
|
||||||
* also note that oversized items do NOT immediately get dropped from
|
|
||||||
* the cache, though they will cause faster turnover in the storage.
|
|
||||||
*/
|
|
||||||
sizeCalculation?: (value: V, key: K) => number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function to call when the item is removed from the cache
|
|
||||||
* Note that using this can negatively impact performance.
|
|
||||||
*/
|
|
||||||
dispose?: (value: V, key: K) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* max time to live for items before they are considered stale
|
|
||||||
* note that stale items are NOT preemptively removed by default,
|
|
||||||
* and MAY live in the cache, contributing to its LRU max, long after
|
|
||||||
* they have expired.
|
|
||||||
* Also, as this cache is optimized for LRU/MRU operations, some of
|
|
||||||
* the staleness/TTL checks will reduce performance, as they will incur
|
|
||||||
* overhead by deleting items.
|
|
||||||
* Must be a positive integer in ms, defaults to 0, which means "no TTL"
|
|
||||||
*/
|
|
||||||
ttl?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return stale items from cache.get() before disposing of them
|
|
||||||
* boolean, default false
|
|
||||||
*/
|
|
||||||
allowStale?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update the age of items on cache.get(), renewing their TTL
|
|
||||||
* boolean, default false
|
|
||||||
*/
|
|
||||||
updateAgeOnGet?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update the age of items on cache.has(), renewing their TTL
|
|
||||||
* boolean, default false
|
|
||||||
*/
|
|
||||||
updateAgeOnHas?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update the "recently-used"-ness of items on cache.has()
|
|
||||||
* boolean, default false
|
|
||||||
*/
|
|
||||||
updateRecencyOnHas?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class GenericLocalMemoryCacher<TFields> implements IEntityGenericCacher<TFields> {
|
|
||||||
constructor(private readonly localMemoryCache: LocalMemoryCache<TFields>) {}
|
|
||||||
|
|
||||||
static createLRUCache<TFields>(
|
|
||||||
options: { maxSize?: number; ttlSeconds?: number } = {}
|
|
||||||
): LocalMemoryCache<TFields> {
|
|
||||||
const DEFAULT_LRU_CACHE_MAX_AGE_SECONDS = 10;
|
|
||||||
const DEFAULT_LRU_CACHE_SIZE = 10000;
|
|
||||||
const maxAgeSeconds = options.ttlSeconds ?? DEFAULT_LRU_CACHE_MAX_AGE_SECONDS;
|
|
||||||
const lruCacheOptions: LRUCacheOptionsV7<string, TFields> = {
|
|
||||||
max: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
|
|
||||||
sizeCalculation: (value: LocalMemoryCacheValue<TFields>) =>
|
|
||||||
value === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE ? 0 : 1,
|
|
||||||
ttl: maxAgeSeconds * 1000, // convert to ms
|
|
||||||
};
|
|
||||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>(lruCacheOptions as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
static createNoOpCache<TFields>(): LocalMemoryCache<TFields> {
|
|
||||||
return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
|
|
||||||
max: 0,
|
|
||||||
maxAge: -1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadManyAsync(
|
|
||||||
keys: readonly string[]
|
|
||||||
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
|
|
||||||
const cacheResults = new Map<string, CacheLoadResult<TFields>>();
|
|
||||||
for (const key of keys) {
|
|
||||||
const cacheResult = this.localMemoryCache.get(key);
|
|
||||||
if (cacheResult === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE) {
|
|
||||||
cacheResults.set(key, {
|
|
||||||
status: CacheStatus.NEGATIVE,
|
|
||||||
});
|
|
||||||
} else if (cacheResult) {
|
|
||||||
cacheResults.set(key, {
|
|
||||||
status: CacheStatus.HIT,
|
|
||||||
item: cacheResult as unknown as TFields,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
cacheResults.set(key, {
|
|
||||||
status: CacheStatus.MISS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cacheResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
|
|
||||||
for (const [key, item] of objectMap) {
|
|
||||||
this.localMemoryCache.set(key, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async cacheDBMissesAsync(keys: readonly string[]): Promise<void> {
|
|
||||||
for (const key of keys) {
|
|
||||||
this.localMemoryCache.set(key, DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
|
|
||||||
for (const key of keys) {
|
|
||||||
this.localMemoryCache.del(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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 PartsCacheAdapter<TFields> {
|
|
||||||
constructor(
|
|
||||||
entityConfiguration: EntityConfiguration<TFields>,
|
|
||||||
localMemoryCache: LocalMemoryCache<TFields>
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
entityConfiguration,
|
|
||||||
new SimplePartsCacher(new GenericLocalMemoryCacher(localMemoryCache))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.entityConfiguration.tableName,
|
|
||||||
`${this.entityConfiguration.cacheKeyVersion}`,
|
|
||||||
columnName,
|
|
||||||
String(fieldValue),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import {
|
|
||||||
computeIfAbsent,
|
|
||||||
EntityCacheAdapter,
|
|
||||||
EntityConfiguration,
|
|
||||||
IEntityCacheAdapterProvider,
|
|
||||||
} from '@expo/entity';
|
|
||||||
|
|
||||||
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
|
|
||||||
import LocalMemoryCacheAdapter from './LocalMemoryCacheAdapter';
|
|
||||||
|
|
||||||
export class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
|
||||||
// local memory cache adapters should be shared/reused across requests
|
|
||||||
static localMemoryCacheAdapterMap = new Map<string, LocalMemoryCacheAdapter<any>>();
|
|
||||||
|
|
||||||
static getNoOpProvider(): IEntityCacheAdapterProvider {
|
|
||||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
|
||||||
GenericLocalMemoryCacher.createNoOpCache<TFields>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getProvider(
|
|
||||||
options: { maxSize?: number; ttlSeconds?: number } = {}
|
|
||||||
): IEntityCacheAdapterProvider {
|
|
||||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
|
||||||
GenericLocalMemoryCacher.createLRUCache<TFields>(options)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
private readonly localMemoryCacheCreator: <TFields>() => LocalMemoryCache<TFields>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public getCacheAdapter<TFields>(
|
|
||||||
entityConfiguration: EntityConfiguration<TFields>
|
|
||||||
): EntityCacheAdapter<TFields> {
|
|
||||||
return computeIfAbsent(
|
|
||||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap,
|
|
||||||
entityConfiguration.tableName,
|
|
||||||
() => {
|
|
||||||
const localMemoryCache = this.localMemoryCacheCreator<TFields>();
|
|
||||||
return new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-180
@@ -1,180 +0,0 @@
|
|||||||
import { CacheStatus, ViewerContext } from '@expo/entity';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import GenericLocalMemoryCacher from '../GenericLocalMemoryCacher';
|
|
||||||
import LocalMemoryCacheAdapter from '../LocalMemoryCacheAdapter';
|
|
||||||
import { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider';
|
|
||||||
import LocalMemoryTestEntity from '../testfixtures/LocalMemoryTestEntity';
|
|
||||||
import {
|
|
||||||
createLocalMemoryIntegrationTestEntityCompanionProvider,
|
|
||||||
createNoopLocalMemoryIntegrationTestEntityCompanionProvider,
|
|
||||||
} from '../testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider';
|
|
||||||
|
|
||||||
class TestViewerContext extends ViewerContext {}
|
|
||||||
|
|
||||||
describe(LocalMemoryCacheAdapter, () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has correct caching behavior', async () => {
|
|
||||||
const viewerContext = new TestViewerContext(
|
|
||||||
createLocalMemoryIntegrationTestEntityCompanionProvider()
|
|
||||||
);
|
|
||||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
|
||||||
LocalMemoryTestEntity,
|
|
||||||
LocalMemoryTestEntity.getCompanionDefinition()
|
|
||||||
)['tableDataCoordinator']['cacheAdapter'];
|
|
||||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
|
||||||
.setField('name', 'blah')
|
|
||||||
.setField('dateField', date)
|
|
||||||
.enforceCreateAsync();
|
|
||||||
|
|
||||||
// loading an entity should put it in cache
|
|
||||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
|
||||||
.enforcing()
|
|
||||||
.loadByIDAsync(entity1Created.getID());
|
|
||||||
|
|
||||||
const entitySpecificGenericCacher =
|
|
||||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
|
||||||
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
|
|
||||||
)!['partsCacher']['cacher'];
|
|
||||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
|
||||||
cacheKeyMaker('id', entity1.getID()),
|
|
||||||
]);
|
|
||||||
const cachedValue = cachedResult.get(cacheKeyMaker('id', entity1.getID()))!;
|
|
||||||
expect(cachedValue).toMatchObject({
|
|
||||||
status: CacheStatus.HIT,
|
|
||||||
item: {
|
|
||||||
id: entity1.getID(),
|
|
||||||
name: 'blah',
|
|
||||||
dateField: date,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// simulate non existent db fetch, should write negative result ('') to cache
|
|
||||||
const nonExistentId = uuidv4();
|
|
||||||
|
|
||||||
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
|
|
||||||
nonExistentId
|
|
||||||
);
|
|
||||||
expect(entityNonExistentResult.ok).toBe(false);
|
|
||||||
|
|
||||||
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
|
||||||
cacheKeyMaker('id', nonExistentId),
|
|
||||||
]);
|
|
||||||
expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
|
|
||||||
status: CacheStatus.NEGATIVE,
|
|
||||||
});
|
|
||||||
|
|
||||||
// load again through entities framework to ensure it reads negative result
|
|
||||||
const entityNonExistentResult2 = await LocalMemoryTestEntity.loader(
|
|
||||||
viewerContext
|
|
||||||
).loadByIDAsync(nonExistentId);
|
|
||||||
expect(entityNonExistentResult2.ok).toBe(false);
|
|
||||||
|
|
||||||
// invalidate from cache to ensure it invalidates correctly
|
|
||||||
await LocalMemoryTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
|
|
||||||
const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync([
|
|
||||||
cacheKeyMaker('id', entity1.getID()),
|
|
||||||
]);
|
|
||||||
const cachedValueMiss = cachedResultMiss.get(cacheKeyMaker('id', entity1.getID()));
|
|
||||||
expect(cachedValueMiss).toMatchObject({ status: CacheStatus.MISS });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shares the cache between different requests', async () => {
|
|
||||||
const genericLocalMemoryCacherLoadManySpy = jest.spyOn(
|
|
||||||
GenericLocalMemoryCacher.prototype as unknown as any,
|
|
||||||
'loadManyAsync'
|
|
||||||
);
|
|
||||||
const viewerContext = new TestViewerContext(
|
|
||||||
createLocalMemoryIntegrationTestEntityCompanionProvider()
|
|
||||||
);
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
|
||||||
.setField('name', 'blah')
|
|
||||||
.setField('dateField', date)
|
|
||||||
.enforceCreateAsync();
|
|
||||||
|
|
||||||
// loading an entity should put it in cache
|
|
||||||
await LocalMemoryTestEntity.loader(viewerContext)
|
|
||||||
.enforcing()
|
|
||||||
.loadByIDAsync(entity1Created.getID());
|
|
||||||
|
|
||||||
// load entity with a different request
|
|
||||||
const viewerContext2 = new TestViewerContext(
|
|
||||||
createLocalMemoryIntegrationTestEntityCompanionProvider()
|
|
||||||
);
|
|
||||||
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2)
|
|
||||||
.enforcing()
|
|
||||||
.loadByIDAsync(entity1Created.getID());
|
|
||||||
|
|
||||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
|
||||||
LocalMemoryTestEntity,
|
|
||||||
LocalMemoryTestEntity.getCompanionDefinition()
|
|
||||||
)['tableDataCoordinator']['cacheAdapter'];
|
|
||||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
|
||||||
expect(entity1WithVc2.getAllFields()).toMatchObject({
|
|
||||||
id: entity1WithVc2.getID(),
|
|
||||||
name: 'blah',
|
|
||||||
dateField: date,
|
|
||||||
});
|
|
||||||
expect(genericLocalMemoryCacherLoadManySpy).toBeCalledWith([
|
|
||||||
cacheKeyMaker('id', entity1WithVc2.getID()),
|
|
||||||
]);
|
|
||||||
expect(genericLocalMemoryCacherLoadManySpy).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('respects the parameters of a noop cache', async () => {
|
|
||||||
const viewerContext = new TestViewerContext(
|
|
||||||
createNoopLocalMemoryIntegrationTestEntityCompanionProvider()
|
|
||||||
);
|
|
||||||
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
|
||||||
LocalMemoryTestEntity,
|
|
||||||
LocalMemoryTestEntity.getCompanionDefinition()
|
|
||||||
)['tableDataCoordinator']['cacheAdapter'];
|
|
||||||
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
|
|
||||||
|
|
||||||
const date = new Date();
|
|
||||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
|
||||||
.setField('name', 'blah')
|
|
||||||
.setField('dateField', date)
|
|
||||||
.enforceCreateAsync();
|
|
||||||
|
|
||||||
// loading an entity will try to put it in cache but it's a noop cache, so it should be a miss
|
|
||||||
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
|
|
||||||
.enforcing()
|
|
||||||
.loadByIDAsync(entity1Created.getID());
|
|
||||||
|
|
||||||
const entitySpecificGenericCacher =
|
|
||||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
|
|
||||||
LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
|
|
||||||
)!['partsCacher']['cacher'];
|
|
||||||
const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
|
||||||
cacheKeyMaker('id', entity1.getID()),
|
|
||||||
]);
|
|
||||||
const cachedValue = cachedResult.get(cacheKeyMaker('id', entity1.getID()))!;
|
|
||||||
expect(cachedValue).toMatchObject({
|
|
||||||
status: CacheStatus.MISS,
|
|
||||||
});
|
|
||||||
|
|
||||||
// a non existent db fetch should try to write negative result ('') but it's a noop cache, so it should be a miss
|
|
||||||
const nonExistentId = uuidv4();
|
|
||||||
|
|
||||||
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
|
|
||||||
nonExistentId
|
|
||||||
);
|
|
||||||
expect(entityNonExistentResult.ok).toBe(false);
|
|
||||||
|
|
||||||
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
|
|
||||||
cacheKeyMaker('id', nonExistentId),
|
|
||||||
]);
|
|
||||||
expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
|
|
||||||
status: CacheStatus.MISS,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-125
@@ -1,125 +0,0 @@
|
|||||||
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
|
|
||||||
|
|
||||||
import GenericLocalMemoryCacher, {
|
|
||||||
DOES_NOT_EXIST_LOCAL_MEMORY_CACHE,
|
|
||||||
} from '../GenericLocalMemoryCacher';
|
|
||||||
import LocalMemoryCacheAdapter from '../LocalMemoryCacheAdapter';
|
|
||||||
|
|
||||||
type BlahFields = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
|
||||||
idField: 'id',
|
|
||||||
tableName: 'blah',
|
|
||||||
schema: {
|
|
||||||
id: new UUIDField({ columnName: 'id', cache: true }),
|
|
||||||
},
|
|
||||||
databaseAdapterFlavor: 'postgres',
|
|
||||||
cacheAdapterFlavor: 'local-memory',
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(LocalMemoryCacheAdapter, () => {
|
|
||||||
describe('loadManyAsync', () => {
|
|
||||||
it('returns appropriate cache results', async () => {
|
|
||||||
const cacheAdapter = new LocalMemoryCacheAdapter(
|
|
||||||
entityConfiguration,
|
|
||||||
GenericLocalMemoryCacher.createLRUCache({
|
|
||||||
maxSize: Number.MAX_SAFE_INTEGER,
|
|
||||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const cacheHits = new Map<string, Readonly<BlahFields>>([['test-id-1', { id: 'test-id-1' }]]);
|
|
||||||
await cacheAdapter.cacheManyAsync('id', cacheHits);
|
|
||||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
|
||||||
|
|
||||||
const results = await cacheAdapter.loadManyAsync('id', [
|
|
||||||
'test-id-1',
|
|
||||||
'test-id-2',
|
|
||||||
'test-id-3',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(results.get('test-id-1')).toMatchObject({
|
|
||||||
status: CacheStatus.HIT,
|
|
||||||
item: { id: 'test-id-1' },
|
|
||||||
});
|
|
||||||
expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.NEGATIVE });
|
|
||||||
expect(results.get('test-id-3')).toMatchObject({ status: CacheStatus.MISS });
|
|
||||||
expect(results.size).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns empty map when passed empty array of fieldValues', async () => {
|
|
||||||
const cacheAdapter = new LocalMemoryCacheAdapter(
|
|
||||||
entityConfiguration,
|
|
||||||
GenericLocalMemoryCacher.createLRUCache({
|
|
||||||
maxSize: Number.MAX_SAFE_INTEGER,
|
|
||||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const results = await cacheAdapter.loadManyAsync('id', []);
|
|
||||||
expect(results).toEqual(new Map());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cacheManyAsync', () => {
|
|
||||||
it('correctly caches all objects', async () => {
|
|
||||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
|
||||||
maxSize: Number.MAX_SAFE_INTEGER,
|
|
||||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
|
||||||
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
|
||||||
|
|
||||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'test-id-1');
|
|
||||||
expect(localMemoryCache.get(cacheKey)).toMatchObject({
|
|
||||||
id: 'test-id-1',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cacheDBMissesAsync', () => {
|
|
||||||
it('correctly caches misses', async () => {
|
|
||||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
|
||||||
maxSize: Number.MAX_SAFE_INTEGER,
|
|
||||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
|
||||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
|
|
||||||
|
|
||||||
const cacheKey = cacheAdapter['partsCacher']['makeCacheKey']('id', 'test-id-1');
|
|
||||||
expect(localMemoryCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('invalidateManyAsync', () => {
|
|
||||||
it('invalidates correctly', async () => {
|
|
||||||
const localMemoryCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
|
||||||
maxSize: Number.MAX_SAFE_INTEGER,
|
|
||||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, localMemoryCache);
|
|
||||||
await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
|
|
||||||
await cacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
|
|
||||||
await cacheAdapter.invalidateManyAsync('id', ['test-id-1', 'test-id-2']);
|
|
||||||
|
|
||||||
const results = await cacheAdapter.loadManyAsync('id', ['test-id-1', 'test-id-2']);
|
|
||||||
expect(results.get('test-id-1')).toMatchObject({ status: CacheStatus.MISS });
|
|
||||||
expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.MISS });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns when passed empty array of fieldValues', async () => {
|
|
||||||
const cacheAdapter = new LocalMemoryCacheAdapter(
|
|
||||||
entityConfiguration,
|
|
||||||
GenericLocalMemoryCacher.createLRUCache<BlahFields>({
|
|
||||||
maxSize: Number.MAX_SAFE_INTEGER,
|
|
||||||
ttlSeconds: Number.MAX_SAFE_INTEGER,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await cacheAdapter.invalidateManyAsync('id', []);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import {
|
|
||||||
AlwaysAllowPrivacyPolicyRule,
|
|
||||||
EntityPrivacyPolicy,
|
|
||||||
ViewerContext,
|
|
||||||
UUIDField,
|
|
||||||
DateField,
|
|
||||||
StringField,
|
|
||||||
EntityConfiguration,
|
|
||||||
EntityCompanionDefinition,
|
|
||||||
Entity,
|
|
||||||
} from '@expo/entity';
|
|
||||||
|
|
||||||
export type LocalMemoryTestEntityFields = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
dateField: Date | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class LocalMemoryTestEntity extends Entity<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext
|
|
||||||
> {
|
|
||||||
static getCompanionDefinition(): EntityCompanionDefinition<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext,
|
|
||||||
LocalMemoryTestEntity,
|
|
||||||
LocalMemoryTestEntityPrivacyPolicy
|
|
||||||
> {
|
|
||||||
return localMemoryTestEntityCompanionDefinition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LocalMemoryTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext,
|
|
||||||
LocalMemoryTestEntity
|
|
||||||
> {
|
|
||||||
protected override readonly createRules = [
|
|
||||||
new AlwaysAllowPrivacyPolicyRule<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext,
|
|
||||||
LocalMemoryTestEntity
|
|
||||||
>(),
|
|
||||||
];
|
|
||||||
protected override readonly readRules = [
|
|
||||||
new AlwaysAllowPrivacyPolicyRule<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext,
|
|
||||||
LocalMemoryTestEntity
|
|
||||||
>(),
|
|
||||||
];
|
|
||||||
protected override readonly updateRules = [
|
|
||||||
new AlwaysAllowPrivacyPolicyRule<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext,
|
|
||||||
LocalMemoryTestEntity
|
|
||||||
>(),
|
|
||||||
];
|
|
||||||
protected override readonly deleteRules = [
|
|
||||||
new AlwaysAllowPrivacyPolicyRule<
|
|
||||||
LocalMemoryTestEntityFields,
|
|
||||||
string,
|
|
||||||
ViewerContext,
|
|
||||||
LocalMemoryTestEntity
|
|
||||||
>(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const localMemoryTestEntityConfiguration =
|
|
||||||
new EntityConfiguration<LocalMemoryTestEntityFields>({
|
|
||||||
idField: 'id',
|
|
||||||
tableName: 'local_memory_test_entities',
|
|
||||||
schema: {
|
|
||||||
id: new UUIDField({
|
|
||||||
columnName: 'id',
|
|
||||||
cache: true,
|
|
||||||
}),
|
|
||||||
name: new StringField({
|
|
||||||
columnName: 'name',
|
|
||||||
cache: true,
|
|
||||||
}),
|
|
||||||
dateField: new DateField({
|
|
||||||
columnName: 'date_field',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
databaseAdapterFlavor: 'postgres',
|
|
||||||
cacheAdapterFlavor: 'local-memory',
|
|
||||||
});
|
|
||||||
|
|
||||||
const localMemoryTestEntityCompanionDefinition = new EntityCompanionDefinition({
|
|
||||||
entityClass: LocalMemoryTestEntity,
|
|
||||||
entityConfiguration: localMemoryTestEntityConfiguration,
|
|
||||||
privacyPolicyClass: LocalMemoryTestEntityPrivacyPolicy,
|
|
||||||
});
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
import {
|
|
||||||
NoOpEntityMetricsAdapter,
|
|
||||||
IEntityMetricsAdapter,
|
|
||||||
EntityCompanionProvider,
|
|
||||||
StubQueryContextProvider,
|
|
||||||
StubDatabaseAdapterProvider,
|
|
||||||
} from '@expo/entity';
|
|
||||||
|
|
||||||
import { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider';
|
|
||||||
|
|
||||||
export const createLocalMemoryIntegrationTestEntityCompanionProvider = (
|
|
||||||
localMemoryOptions: { maxSize?: number; ttlSeconds?: number } = {},
|
|
||||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
|
|
||||||
): EntityCompanionProvider => {
|
|
||||||
const localMemoryCacheAdapterProvider =
|
|
||||||
localMemoryOptions.maxSize === 0 && localMemoryOptions.ttlSeconds === 0
|
|
||||||
? LocalMemoryCacheAdapterProvider.getNoOpProvider()
|
|
||||||
: LocalMemoryCacheAdapterProvider.getProvider(localMemoryOptions);
|
|
||||||
return new EntityCompanionProvider(
|
|
||||||
metricsAdapter,
|
|
||||||
new Map([
|
|
||||||
[
|
|
||||||
'postgres',
|
|
||||||
{
|
|
||||||
adapterProvider: new StubDatabaseAdapterProvider(),
|
|
||||||
queryContextProvider: StubQueryContextProvider,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
new Map([
|
|
||||||
[
|
|
||||||
'local-memory',
|
|
||||||
{
|
|
||||||
cacheAdapterProvider: localMemoryCacheAdapterProvider,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createNoopLocalMemoryIntegrationTestEntityCompanionProvider = (
|
|
||||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
|
|
||||||
): EntityCompanionProvider => {
|
|
||||||
return createLocalMemoryIntegrationTestEntityCompanionProvider(
|
|
||||||
{ maxSize: 0, ttlSeconds: 0 },
|
|
||||||
metricsAdapter
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "build",
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -3,25 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* Refactor cachers to use an abstract class and interface ([#157](https://github.com/expo/entity/pull/157))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity-cache-adapter-redis",
|
"name": "@expo/entity-cache-adapter-redis",
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "Redis cache adapter for @expo/entity",
|
"description": "Redis cache adapter for @expo/entity",
|
||||||
"files": [
|
"files": [
|
||||||
"build",
|
"build",
|
||||||
@@ -33,6 +33,6 @@
|
|||||||
"ioredis": "^4.27.3"
|
"ioredis": "^4.27.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/entity": "^0.24.0"
|
"@expo/entity": "^0.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
import {
|
import { CacheLoadResult, CacheStatus } from '@expo/entity';
|
||||||
CacheLoadResult,
|
|
||||||
CacheStatus,
|
|
||||||
EntityConfiguration,
|
|
||||||
transformCacheObjectToFields,
|
|
||||||
transformFieldsToCacheObject,
|
|
||||||
IEntityGenericCacher,
|
|
||||||
} from '@expo/entity';
|
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
|
|
||||||
import { redisTransformerMap } from './RedisCommon';
|
|
||||||
import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync';
|
import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync';
|
||||||
|
|
||||||
// Sentinel value we store in Redis to negatively cache a database miss.
|
// Sentinel value we store in Redis to negatively cache a database miss.
|
||||||
@@ -34,15 +26,12 @@ export interface GenericRedisCacheContext {
|
|||||||
ttlSecondsNegative: number;
|
ttlSecondsNegative: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GenericRedisCacher<TFields> implements IEntityGenericCacher<TFields> {
|
export default class GenericRedisCacher {
|
||||||
constructor(
|
constructor(private readonly context: GenericRedisCacheContext) {}
|
||||||
private readonly context: GenericRedisCacheContext,
|
|
||||||
private readonly entityConfiguration: EntityConfiguration<TFields>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async loadManyAsync(
|
public async loadManyAsync(
|
||||||
keys: readonly string[]
|
keys: readonly string[]
|
||||||
): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
|
): Promise<ReadonlyMap<string, CacheLoadResult>> {
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return new Map();
|
return new Map();
|
||||||
}
|
}
|
||||||
@@ -51,7 +40,7 @@ export default class GenericRedisCacher<TFields> implements IEntityGenericCacher
|
|||||||
this.context.redisClient.mget(...keys)
|
this.context.redisClient.mget(...keys)
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = new Map<string, CacheLoadResult<TFields>>();
|
const results = new Map<string, CacheLoadResult>();
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const key = keys[i]!;
|
const key = keys[i]!;
|
||||||
const redisResult = redisResults[i];
|
const redisResult = redisResults[i];
|
||||||
@@ -63,11 +52,7 @@ export default class GenericRedisCacher<TFields> implements IEntityGenericCacher
|
|||||||
} else if (redisResult) {
|
} else if (redisResult) {
|
||||||
results.set(key, {
|
results.set(key, {
|
||||||
status: CacheStatus.HIT,
|
status: CacheStatus.HIT,
|
||||||
item: transformCacheObjectToFields(
|
item: JSON.parse(redisResult),
|
||||||
this.entityConfiguration,
|
|
||||||
redisTransformerMap,
|
|
||||||
JSON.parse(redisResult)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
results.set(key, {
|
results.set(key, {
|
||||||
@@ -78,7 +63,7 @@ export default class GenericRedisCacher<TFields> implements IEntityGenericCacher
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
|
public async cacheManyAsync(objectMap: ReadonlyMap<string, object>): Promise<void> {
|
||||||
if (objectMap.size === 0) {
|
if (objectMap.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -87,9 +72,7 @@ export default class GenericRedisCacher<TFields> implements IEntityGenericCacher
|
|||||||
objectMap.forEach((object, key) => {
|
objectMap.forEach((object, key) => {
|
||||||
redisTransaction = redisTransaction.set(
|
redisTransaction = redisTransaction.set(
|
||||||
key,
|
key,
|
||||||
JSON.stringify(
|
JSON.stringify(object),
|
||||||
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
|
|
||||||
),
|
|
||||||
'EX',
|
'EX',
|
||||||
this.context.ttlSecondsPositive
|
this.context.ttlSecondsPositive
|
||||||
);
|
);
|
||||||
@@ -97,7 +80,7 @@ export default class GenericRedisCacher<TFields> implements IEntityGenericCacher
|
|||||||
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
|
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async cacheDBMissesAsync(keys: readonly string[]): Promise<void> {
|
public async cacheDBMissesAsync(keys: string[]): Promise<void> {
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,7 +97,7 @@ export default class GenericRedisCacher<TFields> implements IEntityGenericCacher
|
|||||||
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
|
await wrapNativeRedisCallAsync(() => redisTransaction.exec());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
|
public async invalidateManyAsync(keys: string[]): Promise<void> {
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { EntityConfiguration, PartsCacheAdapter, Parts } from '@expo/entity';
|
import {
|
||||||
import SimplePartsCacher from '@expo/entity/build/SimplePartsCacher';
|
EntityCacheAdapter,
|
||||||
|
EntityConfiguration,
|
||||||
|
CacheLoadResult,
|
||||||
|
FieldTransformerMap,
|
||||||
|
mapKeys,
|
||||||
|
transformCacheObjectToFields,
|
||||||
|
CacheStatus,
|
||||||
|
transformFieldsToCacheObject,
|
||||||
|
} from '@expo/entity';
|
||||||
import invariant from 'invariant';
|
import invariant from 'invariant';
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
|
|
||||||
import GenericRedisCacher from './GenericRedisCacher';
|
import GenericRedisCacher from './GenericRedisCacher';
|
||||||
|
import { redisTransformerMap } from './RedisCommon';
|
||||||
|
|
||||||
export interface RedisCacheAdapterContext {
|
export interface RedisCacheAdapterContext {
|
||||||
/**
|
/**
|
||||||
@@ -42,36 +51,114 @@ export interface RedisCacheAdapterContext {
|
|||||||
ttlSecondsNegative: number;
|
ttlSecondsNegative: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RedisCacheAdapter<TFields> extends PartsCacheAdapter<TFields> {
|
export default class RedisCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||||
|
/**
|
||||||
|
* Transformer definitions for field types. Used to modify values as they are read from or written to
|
||||||
|
* the cache. Override in concrete subclasses to change transformation behavior.
|
||||||
|
* If a field type is not present in the map, then fields of that type will not be transformed.
|
||||||
|
*/
|
||||||
|
private readonly redisFieldTransformer: FieldTransformerMap = redisTransformerMap;
|
||||||
|
private readonly genericRedisCacher: GenericRedisCacher;
|
||||||
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,
|
||||||
{
|
ttlSecondsNegative: context.ttlSecondsNegative,
|
||||||
redisClient: context.redisClient,
|
ttlSecondsPositive: context.ttlSecondsPositive,
|
||||||
ttlSecondsNegative: context.ttlSecondsNegative,
|
});
|
||||||
ttlSecondsPositive: context.ttlSecondsPositive,
|
}
|
||||||
},
|
|
||||||
entityConfiguration
|
public async loadManyAsync<N extends keyof TFields>(
|
||||||
),
|
fieldName: N,
|
||||||
context.makeKeyFn
|
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||||
)
|
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
|
||||||
|
const redisCacheKeyToFieldValueMapping = new Map(
|
||||||
|
fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
|
||||||
|
);
|
||||||
|
const rawCacheResults = await this.genericRedisCacher.loadManyAsync(
|
||||||
|
Array.from(redisCacheKeyToFieldValueMapping.keys())
|
||||||
|
);
|
||||||
|
const cacheResults = new Map<string, CacheLoadResult>();
|
||||||
|
for (const [redisCacheKey, rawCacheResult] of rawCacheResults) {
|
||||||
|
if (rawCacheResult.status === CacheStatus.HIT) {
|
||||||
|
cacheResults.set(redisCacheKey, {
|
||||||
|
status: CacheStatus.HIT,
|
||||||
|
item: transformCacheObjectToFields(
|
||||||
|
this.entityConfiguration,
|
||||||
|
this.redisFieldTransformer,
|
||||||
|
rawCacheResult.item
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cacheResults.set(redisCacheKey, rawCacheResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]>, object>
|
||||||
|
): Promise<void> {
|
||||||
|
const cacheObjectMap = new Map<NonNullable<TFields[N]>, object>();
|
||||||
|
for (const [fieldValue, object] of objectMap) {
|
||||||
|
cacheObjectMap.set(
|
||||||
|
fieldValue,
|
||||||
|
transformFieldsToCacheObject(
|
||||||
|
this.entityConfiguration,
|
||||||
|
this.redisFieldTransformer,
|
||||||
|
object as Readonly<TFields>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.genericRedisCacher.cacheManyAsync(
|
||||||
|
mapKeys(cacheObjectMap, (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.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)
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-125
@@ -1,125 +0,0 @@
|
|||||||
import { CacheStatus, ViewerContext } from '@expo/entity';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { URL } from 'url';
|
|
||||||
|
|
||||||
import GenericRedisCacher from '../GenericRedisCacher';
|
|
||||||
import { RedisCacheAdapterContext } from '../RedisCacheAdapter';
|
|
||||||
import RedisTestEntity, {
|
|
||||||
redisTestEntityConfiguration,
|
|
||||||
RedisTestEntityFields,
|
|
||||||
} from '../testfixtures/RedisTestEntity';
|
|
||||||
import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
|
|
||||||
|
|
||||||
class TestViewerContext extends ViewerContext {}
|
|
||||||
|
|
||||||
describe(GenericRedisCacher, () => {
|
|
||||||
let redisCacheAdapterContext: RedisCacheAdapterContext;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
redisCacheAdapterContext = {
|
|
||||||
redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()),
|
|
||||||
makeKeyFn(...parts: string[]): string {
|
|
||||||
const delimiter = ':';
|
|
||||||
const escapedParts = parts.map((part) =>
|
|
||||||
part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
|
|
||||||
);
|
|
||||||
return escapedParts.join(delimiter);
|
|
||||||
},
|
|
||||||
cacheKeyPrefix: 'test-',
|
|
||||||
cacheKeyVersion: 1,
|
|
||||||
ttlSecondsPositive: 86400, // 1 day
|
|
||||||
ttlSecondsNegative: 600, // 10 minutes
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await redisCacheAdapterContext.redisClient.flushdb();
|
|
||||||
});
|
|
||||||
afterAll(async () => {
|
|
||||||
redisCacheAdapterContext.redisClient.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has correct caching and loading behavior', async () => {
|
|
||||||
const viewerContext = new TestViewerContext(
|
|
||||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
|
||||||
);
|
|
||||||
const genericRedisCacher = new GenericRedisCacher(
|
|
||||||
{
|
|
||||||
redisClient: redisCacheAdapterContext.redisClient,
|
|
||||||
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
|
|
||||||
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
|
|
||||||
},
|
|
||||||
redisTestEntityConfiguration
|
|
||||||
);
|
|
||||||
const date = new Date();
|
|
||||||
const entity1Created = await RedisTestEntity.creator(viewerContext)
|
|
||||||
.setField('name', 'blah')
|
|
||||||
.setField('dateField', date)
|
|
||||||
.enforceCreateAsync();
|
|
||||||
const testKey = `test-id-key-${entity1Created.getID()}`;
|
|
||||||
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
|
|
||||||
[testKey, entity1Created.getAllFields()],
|
|
||||||
]);
|
|
||||||
await genericRedisCacher.cacheManyAsync(objectMap);
|
|
||||||
|
|
||||||
const cachedJSON = await redisCacheAdapterContext.redisClient.get(testKey);
|
|
||||||
const cachedValue = JSON.parse(cachedJSON!);
|
|
||||||
expect(cachedValue).toMatchObject({
|
|
||||||
id: entity1Created.getID(),
|
|
||||||
dateField: date.toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
|
|
||||||
const cacheLoadResult = loadedObjectMap.get(testKey)!;
|
|
||||||
expect(cacheLoadResult).toMatchObject({
|
|
||||||
status: CacheStatus.HIT,
|
|
||||||
item: entity1Created.getAllFields(),
|
|
||||||
});
|
|
||||||
expect(loadedObjectMap.size).toBe(1);
|
|
||||||
});
|
|
||||||
it('has correct negative caching behaviour', async () => {
|
|
||||||
const genericRedisCacher = new GenericRedisCacher(
|
|
||||||
{
|
|
||||||
redisClient: redisCacheAdapterContext.redisClient,
|
|
||||||
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
|
|
||||||
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
|
|
||||||
},
|
|
||||||
redisTestEntityConfiguration
|
|
||||||
);
|
|
||||||
|
|
||||||
const testKey = `test-id-key-non-existent-id`;
|
|
||||||
await genericRedisCacher.cacheDBMissesAsync([testKey]);
|
|
||||||
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
|
|
||||||
const cacheLoadResult = loadedObjectMap.get(testKey)!;
|
|
||||||
expect(cacheLoadResult.status).toBe(CacheStatus.NEGATIVE);
|
|
||||||
});
|
|
||||||
it('has correct invalidation behaviour', async () => {
|
|
||||||
const viewerContext = new TestViewerContext(
|
|
||||||
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
|
|
||||||
);
|
|
||||||
const genericRedisCacher = new GenericRedisCacher(
|
|
||||||
{
|
|
||||||
redisClient: redisCacheAdapterContext.redisClient,
|
|
||||||
ttlSecondsNegative: redisCacheAdapterContext.ttlSecondsNegative,
|
|
||||||
ttlSecondsPositive: redisCacheAdapterContext.ttlSecondsPositive,
|
|
||||||
},
|
|
||||||
redisTestEntityConfiguration
|
|
||||||
);
|
|
||||||
const date = new Date();
|
|
||||||
const entity1Created = await RedisTestEntity.creator(viewerContext)
|
|
||||||
.setField('name', 'blah')
|
|
||||||
.setField('dateField', date)
|
|
||||||
.enforceCreateAsync();
|
|
||||||
const testKey = `test-id-key-${entity1Created.getID()}`;
|
|
||||||
const objectMap = new Map<string, Readonly<RedisTestEntityFields>>([
|
|
||||||
[testKey, entity1Created.getAllFields()],
|
|
||||||
]);
|
|
||||||
await genericRedisCacher.cacheManyAsync(objectMap);
|
|
||||||
|
|
||||||
await genericRedisCacher.invalidateManyAsync([testKey]);
|
|
||||||
|
|
||||||
const loadedObjectMap = await genericRedisCacher.loadManyAsync([testKey]);
|
|
||||||
const cacheLoadResult = loadedObjectMap.get(testKey)!;
|
|
||||||
expect(cacheLoadResult.status).toBe(CacheStatus.MISS);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
|
import { CacheStatus, UUIDField, EntityConfiguration, DateField } from '@expo/entity';
|
||||||
import { Redis, Pipeline } from 'ioredis';
|
import { Redis, Pipeline } from 'ioredis';
|
||||||
import { mock, when, instance, anything, verify } from 'ts-mockito';
|
import { mock, when, instance, anything, verify } from 'ts-mockito';
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import RedisCacheAdapter from '../RedisCacheAdapter';
|
|||||||
|
|
||||||
type BlahFields = {
|
type BlahFields = {
|
||||||
id: string;
|
id: string;
|
||||||
|
date: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
const entityConfiguration = new EntityConfiguration<BlahFields>({
|
||||||
@@ -13,6 +14,7 @@ const entityConfiguration = new EntityConfiguration<BlahFields>({
|
|||||||
tableName: 'blah',
|
tableName: 'blah',
|
||||||
schema: {
|
schema: {
|
||||||
id: new UUIDField({ columnName: 'id', cache: true }),
|
id: new UUIDField({ columnName: 'id', cache: true }),
|
||||||
|
date: new DateField({ columnName: 'date' }),
|
||||||
},
|
},
|
||||||
databaseAdapterFlavor: 'postgres',
|
databaseAdapterFlavor: 'postgres',
|
||||||
cacheAdapterFlavor: 'redis',
|
cacheAdapterFlavor: 'redis',
|
||||||
@@ -39,11 +41,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']);
|
||||||
|
|
||||||
@@ -65,6 +64,40 @@ describe(RedisCacheAdapter, () => {
|
|||||||
const results = await cacheAdapter.loadManyAsync('id', []);
|
const results = await cacheAdapter.loadManyAsync('id', []);
|
||||||
expect(results).toEqual(new Map());
|
expect(results).toEqual(new Map());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('transforms a cache object to an Entity object', async () => {
|
||||||
|
const redisResults = new Map();
|
||||||
|
|
||||||
|
const mockRedisClient = mock<Redis>();
|
||||||
|
|
||||||
|
// need to have one anything() for each element of spread, in this case 1
|
||||||
|
when(mockRedisClient.mget(anything())).thenCall(async (...keys) =>
|
||||||
|
keys.map((k) => redisResults.get(k) ?? null)
|
||||||
|
);
|
||||||
|
|
||||||
|
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
|
||||||
|
redisClient: instance(mockRedisClient),
|
||||||
|
makeKeyFn: (...parts) => parts.join(':'),
|
||||||
|
cacheKeyVersion: 1,
|
||||||
|
cacheKeyPrefix: 'hello-',
|
||||||
|
ttlSecondsPositive: 1,
|
||||||
|
ttlSecondsNegative: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
redisResults.set(
|
||||||
|
cacheAdapter['makeCacheKey']('id', 'wat'),
|
||||||
|
JSON.stringify({ id: 'wat', date: date.toISOString() })
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await cacheAdapter.loadManyAsync('id', ['wat']);
|
||||||
|
|
||||||
|
expect(results.get('wat')).toMatchObject({
|
||||||
|
status: CacheStatus.HIT,
|
||||||
|
item: { id: 'wat', date },
|
||||||
|
});
|
||||||
|
expect(results.size).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cacheManyAsync', () => {
|
describe('cacheManyAsync', () => {
|
||||||
@@ -94,13 +127,48 @@ 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',
|
||||||
ttl: 1,
|
ttl: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('transforms an Entity object to a cache object', async () => {
|
||||||
|
const redisResults = new Map();
|
||||||
|
|
||||||
|
const mockPipeline = mock<Pipeline>();
|
||||||
|
when(mockPipeline.set(anything(), anything(), anything(), anything())).thenCall(
|
||||||
|
(key, value, code, ttl) => {
|
||||||
|
redisResults.set(key, { value, code, ttl });
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
when(mockPipeline.exec()).thenResolve({} as any);
|
||||||
|
const pipeline = instance(mockPipeline);
|
||||||
|
|
||||||
|
const mockRedisClient = mock<Redis>();
|
||||||
|
when(mockRedisClient.multi()).thenReturn(pipeline);
|
||||||
|
|
||||||
|
const cacheAdapter = new RedisCacheAdapter(entityConfiguration, {
|
||||||
|
redisClient: instance(mockRedisClient),
|
||||||
|
makeKeyFn: (...parts) => parts.join(':'),
|
||||||
|
cacheKeyVersion: 1,
|
||||||
|
cacheKeyPrefix: 'hello-',
|
||||||
|
ttlSecondsPositive: 1,
|
||||||
|
ttlSecondsNegative: 2,
|
||||||
|
});
|
||||||
|
const date = new Date();
|
||||||
|
await cacheAdapter.cacheManyAsync('id', new Map([['wat', { id: 'wat', date }]]));
|
||||||
|
|
||||||
|
const cacheKey = cacheAdapter['makeCacheKey']('id', 'wat');
|
||||||
|
expect(redisResults.get(cacheKey)).toMatchObject({
|
||||||
|
value: JSON.stringify({ id: 'wat', date: date.toISOString() }),
|
||||||
|
code: 'EX',
|
||||||
|
ttl: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cacheDBMissesAsync', () => {
|
describe('cacheDBMissesAsync', () => {
|
||||||
@@ -129,7 +197,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 +221,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity-database-adapter-knex",
|
"name": "@expo/entity-database-adapter-knex",
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "Knex database adapter for @expo/entity",
|
"description": "Knex database adapter for @expo/entity",
|
||||||
"files": [
|
"files": [
|
||||||
"build",
|
"build",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"knex": "^1.0.2"
|
"knex": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/entity": "^0.24.0",
|
"@expo/entity": "^0.22.0",
|
||||||
"pg": "^8.7.1"
|
"pg": "^8.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,22 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-example
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-example
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-example
|
**Note:** Version bump only for package @expo/entity-example
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity-example",
|
"name": "@expo/entity-example",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "An example integration of the @expo/entity framework",
|
"description": "An example integration of the @expo/entity framework",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"author": "Expo",
|
"author": "Expo",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/entity": "^0.24.0",
|
"@expo/entity": "^0.22.0",
|
||||||
"apollo-server-koa": "^2.16.1",
|
"apollo-server-koa": "^2.16.1",
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"koa": "^2.13.0",
|
"koa": "^2.13.0",
|
||||||
|
|||||||
@@ -3,22 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity-full-integration-tests",
|
"name": "@expo/entity-full-integration-tests",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "Full redis and knex integration tests for the entity framework",
|
"description": "Full redis and knex integration tests for the entity framework",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
"@expo/entity-database-adapter-knex": "*"
|
"@expo/entity-database-adapter-knex": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/entity": "^0.24.0",
|
"@expo/entity": "^0.22.0",
|
||||||
"@expo/entity-cache-adapter-redis": "^0.24.0",
|
"@expo/entity-cache-adapter-redis": "^0.22.0",
|
||||||
"@expo/entity-database-adapter-knex": "^0.24.0"
|
"@expo/entity-database-adapter-knex": "^0.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,22 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
**Note:** Version bump only for package @expo/entity-ip-address-field
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity-ip-address-field",
|
"name": "@expo/entity-ip-address-field",
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "IP address EntityField definitions for @expo/entity",
|
"description": "IP address EntityField definitions for @expo/entity",
|
||||||
"files": [
|
"files": [
|
||||||
"build",
|
"build",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"ip-address": "^8.1.0"
|
"ip-address": "^8.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/entity": "^0.24.0",
|
"@expo/entity": "^0.22.0",
|
||||||
"@types/jsbn": "^1.2.29"
|
"@types/jsbn": "^1.2.29"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -3,24 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* Refactor cachers to use an abstract class and interface ([#157](https://github.com/expo/entity/pull/157))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* Move data transforming responsibilities to the CacheAdapter instead of the RedisSecondaryEntityCache. ([#153](https://github.com/expo/entity/pull/153))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
|
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity-secondary-cache-redis",
|
"name": "@expo/entity-secondary-cache-redis",
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "Redis secondary cache for @expo/entity",
|
"description": "Redis secondary cache for @expo/entity",
|
||||||
"files": [
|
"files": [
|
||||||
"build",
|
"build",
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"ioredis": "^4.17.3"
|
"ioredis": "^4.17.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@expo/entity": "^0.24.0",
|
"@expo/entity": "^0.22.0",
|
||||||
"@expo/entity-cache-adapter-redis": "^0.24.0",
|
"@expo/entity-cache-adapter-redis": "^0.22.0",
|
||||||
"nullthrows": "^1.1.1"
|
"nullthrows": "^1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,136 @@
|
|||||||
import {
|
import {
|
||||||
|
CacheStatus,
|
||||||
EntityConfiguration,
|
EntityConfiguration,
|
||||||
GenericSecondaryEntityCache,
|
filterMap,
|
||||||
Parts,
|
ISecondaryEntityCache,
|
||||||
SimplePartsCacher,
|
transformCacheObjectToFields,
|
||||||
|
transformFieldsToCacheObject,
|
||||||
|
zipToMap,
|
||||||
} from '@expo/entity';
|
} from '@expo/entity';
|
||||||
import { GenericRedisCacheContext, GenericRedisCacher } from '@expo/entity-cache-adapter-redis';
|
import {
|
||||||
|
GenericRedisCacheContext,
|
||||||
|
GenericRedisCacher,
|
||||||
|
redisTransformerMap,
|
||||||
|
} from '@expo/entity-cache-adapter-redis';
|
||||||
|
import invariant from 'invariant';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* single entity load. One common way this may be used is to add a second layer of caching in a hot path that makes
|
* single entity load. One common way this may be used is to add a second layer of caching in a hot path that makes
|
||||||
* a call to {@link EntityLoader.loadManyByFieldEqualityConjunctionAsync} is guaranteed to return at most one entity.
|
* a call to {@link EntityLoader.loadManyByFieldEqualityConjunctionAsync} is guaranteed to return at most one entity.
|
||||||
*/
|
*/
|
||||||
export default class RedisSecondaryEntityCache<
|
export default class RedisSecondaryEntityCache<TFields, TLoadParams>
|
||||||
TFields,
|
implements ISecondaryEntityCache<TFields, TLoadParams>
|
||||||
TLoadParams
|
{
|
||||||
> extends GenericSecondaryEntityCache<TFields, TLoadParams> {
|
private readonly genericRedisCacher: GenericRedisCacher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
entityConfiguration: EntityConfiguration<TFields>,
|
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
||||||
genericRedisCacheContext: GenericRedisCacheContext,
|
genericRedisCacheContext: GenericRedisCacheContext,
|
||||||
makeRedisSpecificKey: (...parts: Parts) => string,
|
private readonly constructRedisKey: (params: Readonly<TLoadParams>) => string
|
||||||
getParts: (params: Readonly<TLoadParams>) => Parts
|
|
||||||
) {
|
) {
|
||||||
super(
|
this.genericRedisCacher = new GenericRedisCacher(genericRedisCacheContext);
|
||||||
new SimplePartsCacher(
|
}
|
||||||
new GenericRedisCacher(genericRedisCacheContext, entityConfiguration),
|
|
||||||
makeRedisSpecificKey
|
/**
|
||||||
),
|
* Read-through cache function. Steps:
|
||||||
getParts
|
*
|
||||||
|
* 1. Check for cached objects
|
||||||
|
* 2. Query the fetcher for loadParams not in the cache
|
||||||
|
* 3. Cache the results from the fetcher
|
||||||
|
* 4. Negatively cache anything missing from the fetcher
|
||||||
|
* 5. Return the full set of data for the query.
|
||||||
|
*
|
||||||
|
* @param loadParamsArray - array of loadParams to load from the cache
|
||||||
|
* @param fetcher - closure used to provide underlying data source objects for loadParams
|
||||||
|
* @returns map from loadParams to the entity field object
|
||||||
|
*/
|
||||||
|
public async loadManyThroughAsync(
|
||||||
|
loadParamsArray: readonly Readonly<TLoadParams>[],
|
||||||
|
fetcher: (
|
||||||
|
fetcherLoadParamsArray: readonly Readonly<TLoadParams>[]
|
||||||
|
) => Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>>
|
||||||
|
): Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> {
|
||||||
|
const redisKeys = loadParamsArray.map(this.constructRedisKey);
|
||||||
|
const redisKeyToLoadParamsMap = zipToMap(redisKeys, loadParamsArray);
|
||||||
|
|
||||||
|
const cacheLoadResults = await this.genericRedisCacher.loadManyAsync(redisKeys);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
cacheLoadResults.size === loadParamsArray.length,
|
||||||
|
`${this.constructor.name} loadMany should return a result for each key`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const redisKeysToFetch = Array.from(
|
||||||
|
filterMap(
|
||||||
|
cacheLoadResults,
|
||||||
|
(cacheLoadResult) => cacheLoadResult.status === CacheStatus.MISS
|
||||||
|
).keys()
|
||||||
|
);
|
||||||
|
|
||||||
|
// put transformed cache hits in result map
|
||||||
|
const results: Map<Readonly<TLoadParams>, Readonly<TFields> | null> = new Map();
|
||||||
|
cacheLoadResults.forEach((cacheLoadResult, redisKey) => {
|
||||||
|
if (cacheLoadResult.status === CacheStatus.HIT) {
|
||||||
|
const loadParams = redisKeyToLoadParamsMap.get(redisKey);
|
||||||
|
invariant(loadParams !== undefined, 'load params should be in redis key map');
|
||||||
|
results.set(
|
||||||
|
loadParams,
|
||||||
|
transformCacheObjectToFields(
|
||||||
|
this.entityConfiguration,
|
||||||
|
redisTransformerMap,
|
||||||
|
cacheLoadResult.item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetch any misses from DB, add DB objects to results, cache DB results, inform cache of any missing DB results
|
||||||
|
if (redisKeysToFetch.length > 0) {
|
||||||
|
const loadParamsToFetch = redisKeysToFetch.map((redisKey) => {
|
||||||
|
const loadParams = redisKeyToLoadParamsMap.get(redisKey);
|
||||||
|
invariant(loadParams !== undefined, 'load params should be in redis key map');
|
||||||
|
return loadParams;
|
||||||
|
});
|
||||||
|
const fetchResults = await fetcher(loadParamsToFetch);
|
||||||
|
|
||||||
|
const fetchMisses = loadParamsToFetch.filter((loadParams) => {
|
||||||
|
// all values of fetchResults should be field objects or undefined
|
||||||
|
return !fetchResults.get(loadParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const fetchMiss of fetchMisses) {
|
||||||
|
results.set(fetchMiss, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectsToCache: Map<string, object> = new Map();
|
||||||
|
for (const [loadParams, object] of fetchResults.entries()) {
|
||||||
|
if (object) {
|
||||||
|
objectsToCache.set(
|
||||||
|
this.constructRedisKey(loadParams),
|
||||||
|
transformFieldsToCacheObject(this.entityConfiguration, redisTransformerMap, object)
|
||||||
|
);
|
||||||
|
results.set(loadParams, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.genericRedisCacher.cacheManyAsync(objectsToCache),
|
||||||
|
this.genericRedisCacher.cacheDBMissesAsync(fetchMisses.map(this.constructRedisKey)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the cache for objects cached by constructRedisKey(loadParams).
|
||||||
|
*
|
||||||
|
* @param loadParamsArray - load params to invalidate
|
||||||
|
*/
|
||||||
|
public async invalidateManyAsync(
|
||||||
|
loadParamsArray: readonly Readonly<TLoadParams>[]
|
||||||
|
): Promise<void> {
|
||||||
|
await this.genericRedisCacher.invalidateManyAsync(loadParamsArray.map(this.constructRedisKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-4
@@ -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)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,27 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
# [0.24.0](https://github.com/expo/entity/compare/v0.23.0...v0.24.0) (2022-02-16)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Chores
|
|
||||||
|
|
||||||
* Refactor cachers to use an abstract class and interface ([#157](https://github.com/expo/entity/pull/157))
|
|
||||||
|
|
||||||
# [0.23.0](https://github.com/expo/entity/compare/v0.22.0...v0.23.0) (2022-02-09)
|
|
||||||
|
|
||||||
### Breaking changes
|
|
||||||
|
|
||||||
* Move data transforming responsibilities to the CacheAdapter instead of the ReadThroughEntityCache. Affects any classes extending the EntityCacheAdapter and relying on the transformation behavior of the ReadThroughEntityCache. ([#153](https://github.com/expo/entity/pull/153))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
# [0.22.0](https://github.com/expo/entity/compare/v0.21.0...v0.22.0) (2022-02-04)
|
||||||
|
|
||||||
**Note:** Version bump only for package @expo/entity
|
**Note:** Version bump only for package @expo/entity
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@expo/entity",
|
"name": "@expo/entity",
|
||||||
"version": "0.24.0",
|
"version": "0.22.0",
|
||||||
"description": "A privacy-first data model",
|
"description": "A privacy-first data model",
|
||||||
"files": [
|
"files": [
|
||||||
"build",
|
"build",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ export default abstract class EntityCacheAdapter<TFields> {
|
|||||||
public abstract loadManyAsync<N extends keyof TFields>(
|
public abstract loadManyAsync<N extends keyof TFields>(
|
||||||
fieldName: N,
|
fieldName: N,
|
||||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>>;
|
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache many objects fetched from the DB.
|
* Cache many objects fetched from the DB.
|
||||||
@@ -26,7 +26,7 @@ export default abstract class EntityCacheAdapter<TFields> {
|
|||||||
*/
|
*/
|
||||||
public abstract cacheManyAsync<N extends keyof TFields>(
|
public abstract cacheManyAsync<N extends keyof TFields>(
|
||||||
fieldName: N,
|
fieldName: N,
|
||||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ export enum EntityEdgeDeletionBehavior {
|
|||||||
SET_NULL,
|
SET_NULL,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines an association between entities. An association is primarily used to define cascading deletion behavior.
|
|
||||||
*/
|
|
||||||
export interface EntityAssociationDefinition<
|
export interface EntityAssociationDefinition<
|
||||||
TViewerContext extends ViewerContext,
|
TViewerContext extends ViewerContext,
|
||||||
TAssociatedFields,
|
TAssociatedFields,
|
||||||
@@ -86,10 +83,6 @@ export interface EntityAssociationDefinition<
|
|||||||
edgeDeletionBehavior?: EntityEdgeDeletionBehavior;
|
edgeDeletionBehavior?: EntityEdgeDeletionBehavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Definition for a field referencing a column in the underlying database. Specifies things like
|
|
||||||
* cache behavior and associations, and handles input validation.
|
|
||||||
*/
|
|
||||||
export abstract class EntityFieldDefinition<T> {
|
export abstract class EntityFieldDefinition<T> {
|
||||||
readonly columnName: string;
|
readonly columnName: string;
|
||||||
readonly cache: boolean;
|
readonly cache: boolean;
|
||||||
@@ -100,7 +93,6 @@ export abstract class EntityFieldDefinition<T> {
|
|||||||
* @param cache - Whether or not to cache loaded instances of the entity by this field. The column name is
|
* @param cache - Whether or not to cache loaded instances of the entity by this field. The column name is
|
||||||
* used to derive a cache key for the cache entry. If true, this column must be able uniquely
|
* used to derive a cache key for the cache entry. If true, this column must be able uniquely
|
||||||
* identify the entity.
|
* identify the entity.
|
||||||
* @param association - Defines the association behavior for an entity that this column references.
|
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
columnName,
|
columnName,
|
||||||
|
|||||||
@@ -2,104 +2,59 @@ import { validate as validateUUID } from 'uuid';
|
|||||||
|
|
||||||
import { EntityFieldDefinition } from './EntityFieldDefinition';
|
import { EntityFieldDefinition } from './EntityFieldDefinition';
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS string type.
|
|
||||||
*/
|
|
||||||
export class StringField extends EntityFieldDefinition<string> {
|
export class StringField extends EntityFieldDefinition<string> {
|
||||||
protected validateInputValueInternal(value: string): boolean {
|
protected validateInputValueInternal(value: string): boolean {
|
||||||
return typeof value === 'string';
|
return typeof value === 'string';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS string type.
|
|
||||||
* Enforces that the string is a valid UUID.
|
|
||||||
*/
|
|
||||||
export class UUIDField extends StringField {
|
export class UUIDField extends StringField {
|
||||||
protected override validateInputValueInternal(value: string): boolean {
|
protected override validateInputValueInternal(value: string): boolean {
|
||||||
return validateUUID(value);
|
return validateUUID(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS Date type.
|
|
||||||
*/
|
|
||||||
export class DateField extends EntityFieldDefinition<Date> {
|
export class DateField extends EntityFieldDefinition<Date> {
|
||||||
protected validateInputValueInternal(value: Date): boolean {
|
protected validateInputValueInternal(value: Date): boolean {
|
||||||
return value instanceof Date;
|
return value instanceof Date;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS boolean type.
|
|
||||||
*/
|
|
||||||
export class BooleanField extends EntityFieldDefinition<boolean> {
|
export class BooleanField extends EntityFieldDefinition<boolean> {
|
||||||
protected validateInputValueInternal(value: boolean): boolean {
|
protected validateInputValueInternal(value: boolean): boolean {
|
||||||
return typeof value === 'boolean';
|
return typeof value === 'boolean';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS number type.
|
|
||||||
* Enforces that the number is an integer.
|
|
||||||
*/
|
|
||||||
export class IntField extends EntityFieldDefinition<number> {
|
export class IntField extends EntityFieldDefinition<number> {
|
||||||
protected validateInputValueInternal(value: number): boolean {
|
protected validateInputValueInternal(value: number): boolean {
|
||||||
return typeof value === 'number' && Number.isInteger(value);
|
return typeof value === 'number' && Number.isInteger(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS number type.
|
|
||||||
* Enforces that the number is a float (which includes integers in JS).
|
|
||||||
*/
|
|
||||||
export class FloatField extends EntityFieldDefinition<number> {
|
export class FloatField extends EntityFieldDefinition<number> {
|
||||||
protected validateInputValueInternal(value: number): boolean {
|
protected validateInputValueInternal(value: number): boolean {
|
||||||
return typeof value === 'number';
|
return typeof value === 'number';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS string array type.
|
|
||||||
* Enforces that every member of the string array is a string.
|
|
||||||
*/
|
|
||||||
export class StringArrayField extends EntityFieldDefinition<string[]> {
|
export class StringArrayField extends EntityFieldDefinition<string[]> {
|
||||||
protected validateInputValueInternal(value: string[]): boolean {
|
protected validateInputValueInternal(value: string[]): boolean {
|
||||||
return Array.isArray(value) && value.every((subValue) => typeof subValue === 'string');
|
return Array.isArray(value) && value.every((subValue) => typeof subValue === 'string');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS JSON object type.
|
|
||||||
*/
|
|
||||||
export class JSONObjectField extends EntityFieldDefinition<object> {
|
export class JSONObjectField extends EntityFieldDefinition<object> {
|
||||||
protected validateInputValueInternal(value: object): boolean {
|
protected validateInputValueInternal(value: object): boolean {
|
||||||
return typeof value === 'object' && !Array.isArray(value);
|
return typeof value === 'object' && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a enum column with a JS string or number type.
|
|
||||||
*/
|
|
||||||
export class EnumField extends EntityFieldDefinition<string | number> {
|
export class EnumField extends EntityFieldDefinition<string | number> {
|
||||||
protected validateInputValueInternal(value: string | number): boolean {
|
protected validateInputValueInternal(value: string | number): boolean {
|
||||||
return typeof value === 'number' || typeof value === 'string';
|
return typeof value === 'number' || typeof value === 'string';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column with a JS JSON array type.
|
|
||||||
*/
|
|
||||||
export class JSONArrayField extends EntityFieldDefinition<any[]> {
|
export class JSONArrayField extends EntityFieldDefinition<any[]> {
|
||||||
protected validateInputValueInternal(value: any[]): boolean {
|
protected validateInputValueInternal(value: any[]): boolean {
|
||||||
return Array.isArray(value);
|
return Array.isArray(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link EntityFieldDefinition} for a column that may be a JS JSON array type.
|
|
||||||
* Does not do any validation.
|
|
||||||
*/
|
|
||||||
export class MaybeJSONArrayField extends EntityFieldDefinition<any | any[]> {
|
export class MaybeJSONArrayField extends EntityFieldDefinition<any | any[]> {
|
||||||
protected validateInputValueInternal(_value: any): boolean {
|
protected validateInputValueInternal(_value: any): boolean {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import invariant from 'invariant';
|
|
||||||
|
|
||||||
import { ISecondaryEntityCache } from './EntitySecondaryCacheLoader';
|
|
||||||
import PartsCacher, { Parts, PartsKey } from './PartsCacher';
|
|
||||||
import { CacheStatus } from './internal/ReadThroughEntityCache';
|
|
||||||
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
|
|
||||||
* single entity load. One common way this may be used is to add a second layer of caching in a hot path that makes
|
|
||||||
* a call to {@link EntityLoader.loadManyByFieldEqualityConjunctionAsync} is guaranteed to return at most one entity.
|
|
||||||
*/
|
|
||||||
export default abstract class GenericSecondaryEntityCache<TFields, TLoadParams>
|
|
||||||
implements ISecondaryEntityCache<TFields, TLoadParams>
|
|
||||||
{
|
|
||||||
constructor(
|
|
||||||
protected readonly partsCacher: PartsCacher<TFields>,
|
|
||||||
protected readonly getParts: (params: Readonly<TLoadParams>) => Parts
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async loadManyThroughAsync(
|
|
||||||
loadParamsArray: readonly Readonly<TLoadParams>[],
|
|
||||||
fetcher: (
|
|
||||||
fetcherLoadParamsArray: readonly Readonly<TLoadParams>[]
|
|
||||||
) => Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>>
|
|
||||||
): Promise<ReadonlyMap<Readonly<TLoadParams>, Readonly<TFields> | null>> {
|
|
||||||
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.partsCacher.loadManyAsync(partsList);
|
|
||||||
|
|
||||||
invariant(
|
|
||||||
cacheLoadResults.size === loadParamsArray.length,
|
|
||||||
`${this.constructor.name} loadMany should return a result for each key`
|
|
||||||
);
|
|
||||||
|
|
||||||
const partsKeysToFetch = Array.from(
|
|
||||||
filterMap(
|
|
||||||
cacheLoadResults,
|
|
||||||
(cacheLoadResult) => cacheLoadResult.status === CacheStatus.MISS
|
|
||||||
).keys()
|
|
||||||
);
|
|
||||||
|
|
||||||
// put cache hits in result map
|
|
||||||
const results: Map<Readonly<TLoadParams>, Readonly<TFields> | null> = new Map();
|
|
||||||
cacheLoadResults.forEach((cacheLoadResult, partsKey) => {
|
|
||||||
if (cacheLoadResult.status === CacheStatus.HIT) {
|
|
||||||
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 (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;
|
|
||||||
});
|
|
||||||
const fetchResults = await fetcher(loadParamsToFetch);
|
|
||||||
|
|
||||||
const fetchMisses = loadParamsToFetch.filter((loadParams) => {
|
|
||||||
// all values of fetchResults should be field objects or undefined
|
|
||||||
return !fetchResults.get(loadParams);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const fetchMiss of fetchMisses) {
|
|
||||||
results.set(fetchMiss, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectsToCache: Map<PartsKey, Readonly<TFields>> = new Map();
|
|
||||||
for (const [loadParams, object] of fetchResults.entries()) {
|
|
||||||
if (object) {
|
|
||||||
const parts = this.getParts(loadParams);
|
|
||||||
objectsToCache.set(PartsCacher.getPartsKey(...parts), object);
|
|
||||||
results.set(loadParams, object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
this.partsCacher.cacheManyAsync(objectsToCache),
|
|
||||||
this.partsCacher.cacheDBMissesAsync(
|
|
||||||
fetchMisses.map((loadParams) => this.getParts(loadParams))
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate the cache for objects cached by constructCacheKey(loadParams).
|
|
||||||
*
|
|
||||||
* @param loadParamsArray - load params to invalidate
|
|
||||||
*/
|
|
||||||
public invalidateManyAsync(loadParamsArray: readonly Readonly<TLoadParams>[]): Promise<void> {
|
|
||||||
const partsList = loadParamsArray.map(this.getParts);
|
|
||||||
return this.partsCacher.invalidateManyAsync(partsList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { CacheLoadResult } from './internal/ReadThroughEntityCache';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A cacher stores and loads key-value pairs. It also supports negative caching - it stores the absence
|
|
||||||
* of keys that don't exist in the backing datastore.
|
|
||||||
*/
|
|
||||||
export default interface IEntityGenericCacher<TFields> {
|
|
||||||
loadManyAsync(keys: readonly string[]): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>>;
|
|
||||||
|
|
||||||
cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void>;
|
|
||||||
|
|
||||||
cacheDBMissesAsync(keys: readonly string[]): Promise<void>;
|
|
||||||
|
|
||||||
invalidateManyAsync(keys: readonly string[]): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
* @module @expo/entity
|
* @module @expo/entity
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as GenericSecondaryEntityCache } from './GenericSecondaryEntityCache';
|
|
||||||
export { default as EnforcingEntityLoader } from './EnforcingEntityLoader';
|
export { default as EnforcingEntityLoader } from './EnforcingEntityLoader';
|
||||||
export { default as Entity } from './Entity';
|
export { default as Entity } from './Entity';
|
||||||
export * from './Entity';
|
export * from './Entity';
|
||||||
@@ -44,12 +43,6 @@ export * from './EntityQueryContext';
|
|||||||
export { default as IEntityCacheAdapterProvider } from './IEntityCacheAdapterProvider';
|
export { default as IEntityCacheAdapterProvider } from './IEntityCacheAdapterProvider';
|
||||||
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 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';
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export enum CacheStatus {
|
|||||||
NEGATIVE,
|
NEGATIVE,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CacheLoadResult<TFields> =
|
export type CacheLoadResult =
|
||||||
| {
|
| {
|
||||||
status: CacheStatus.HIT;
|
status: CacheStatus.HIT;
|
||||||
item: Readonly<TFields>;
|
item: Readonly<object>;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
status: CacheStatus.MISS;
|
status: CacheStatus.MISS;
|
||||||
@@ -82,7 +82,7 @@ export default class ReadThroughEntityCache<TFields> {
|
|||||||
const results: Map<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> = new Map();
|
const results: Map<NonNullable<TFields[N]>, readonly Readonly<TFields>[]> = new Map();
|
||||||
cacheLoadResults.forEach((cacheLoadResult, fieldValue) => {
|
cacheLoadResults.forEach((cacheLoadResult, fieldValue) => {
|
||||||
if (cacheLoadResult.status === CacheStatus.HIT) {
|
if (cacheLoadResult.status === CacheStatus.HIT) {
|
||||||
results.set(fieldValue, [cacheLoadResult.item]);
|
results.set(fieldValue, [cacheLoadResult.item as Readonly<TFields>]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ export default class ReadThroughEntityCache<TFields> {
|
|||||||
return !objectsFromFulfillerForFv || objectsFromFulfillerForFv.length === 0;
|
return !objectsFromFulfillerForFv || objectsFromFulfillerForFv.length === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const objectsToCache: Map<NonNullable<TFields[N]>, Readonly<TFields>> = new Map();
|
const objectsToCache: Map<NonNullable<TFields[N]>, object> = new Map();
|
||||||
for (const [fieldValue, objects] of dbFetchResults.entries()) {
|
for (const [fieldValue, objects] of dbFetchResults.entries()) {
|
||||||
if (objects.length > 1) {
|
if (objects.length > 1) {
|
||||||
// multiple objects received for what was supposed to be a unique query, don't add to return map nor cache
|
// multiple objects received for what was supposed to be a unique query, don't add to return map nor cache
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import invariant from 'invariant';
|
|||||||
import EntityCacheAdapter from '../../EntityCacheAdapter';
|
import EntityCacheAdapter from '../../EntityCacheAdapter';
|
||||||
import EntityConfiguration from '../../EntityConfiguration';
|
import EntityConfiguration from '../../EntityConfiguration';
|
||||||
import IEntityCacheAdapterProvider from '../../IEntityCacheAdapterProvider';
|
import IEntityCacheAdapterProvider from '../../IEntityCacheAdapterProvider';
|
||||||
|
import { FieldTransformerMap } from '../../internal/EntityFieldTransformationUtils';
|
||||||
import { CacheStatus, CacheLoadResult } from '../../internal/ReadThroughEntityCache';
|
import { CacheStatus, CacheLoadResult } from '../../internal/ReadThroughEntityCache';
|
||||||
|
|
||||||
export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||||
@@ -14,11 +15,15 @@ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||||
|
public getFieldTransformerMap(): FieldTransformerMap {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
public async loadManyAsync<N extends keyof TFields>(
|
public async loadManyAsync<N extends keyof TFields>(
|
||||||
_fieldName: N,
|
_fieldName: N,
|
||||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
|
||||||
return fieldValues.reduce((acc: Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>, v) => {
|
return fieldValues.reduce((acc: Map<NonNullable<TFields[N]>, CacheLoadResult>, v) => {
|
||||||
acc.set(v, {
|
acc.set(v, {
|
||||||
status: CacheStatus.MISS,
|
status: CacheStatus.MISS,
|
||||||
});
|
});
|
||||||
@@ -28,7 +33,7 @@ export class NoCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields
|
|||||||
|
|
||||||
public async cacheManyAsync<N extends keyof TFields>(
|
public async cacheManyAsync<N extends keyof TFields>(
|
||||||
_fieldName: N,
|
_fieldName: N,
|
||||||
_objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
_objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||||
): Promise<void> {}
|
): Promise<void> {}
|
||||||
|
|
||||||
public async cacheDBMissesAsync<N extends keyof TFields>(
|
public async cacheDBMissesAsync<N extends keyof TFields>(
|
||||||
@@ -48,26 +53,27 @@ export class InMemoryFullCacheStubCacheAdapterProvider implements IEntityCacheAd
|
|||||||
getCacheAdapter<TFields>(
|
getCacheAdapter<TFields>(
|
||||||
entityConfiguration: EntityConfiguration<TFields>
|
entityConfiguration: EntityConfiguration<TFields>
|
||||||
): EntityCacheAdapter<TFields> {
|
): EntityCacheAdapter<TFields> {
|
||||||
return new InMemoryFullCacheStubCacheAdapter(
|
return new InMemoryFullCacheStubCacheAdapter(entityConfiguration, this.cache);
|
||||||
entityConfiguration,
|
|
||||||
this.cache as Map<string, Readonly<TFields>>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
|
||||||
constructor(
|
constructor(
|
||||||
entityConfiguration: EntityConfiguration<TFields>,
|
entityConfiguration: EntityConfiguration<TFields>,
|
||||||
readonly cache: Map<string, Readonly<TFields>>
|
readonly cache: Map<string, Readonly<object>>
|
||||||
) {
|
) {
|
||||||
super(entityConfiguration);
|
super(entityConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFieldTransformerMap(): FieldTransformerMap {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
public async loadManyAsync<N extends keyof TFields>(
|
public async loadManyAsync<N extends keyof TFields>(
|
||||||
fieldName: N,
|
fieldName: N,
|
||||||
fieldValues: readonly NonNullable<TFields[N]>[]
|
fieldValues: readonly NonNullable<TFields[N]>[]
|
||||||
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
|
): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult>> {
|
||||||
const results = new Map<NonNullable<TFields[N]>, CacheLoadResult<TFields>>();
|
const results = new Map<NonNullable<TFields[N]>, CacheLoadResult>();
|
||||||
fieldValues.forEach((fieldValue) => {
|
fieldValues.forEach((fieldValue) => {
|
||||||
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
||||||
if (!this.cache.has(cacheKey)) {
|
if (!this.cache.has(cacheKey)) {
|
||||||
@@ -88,7 +94,7 @@ export class InMemoryFullCacheStubCacheAdapter<TFields> extends EntityCacheAdapt
|
|||||||
|
|
||||||
public async cacheManyAsync<N extends keyof TFields>(
|
public async cacheManyAsync<N extends keyof TFields>(
|
||||||
fieldName: N,
|
fieldName: N,
|
||||||
objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
|
objectMap: ReadonlyMap<NonNullable<TFields[N]>, object>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
objectMap.forEach((obj, fieldValue) => {
|
objectMap.forEach((obj, fieldValue) => {
|
||||||
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
const cacheKey = this.createCacheKey(fieldName, fieldValue);
|
||||||
|
|||||||
@@ -507,33 +507,28 @@
|
|||||||
exec-sh "^0.3.2"
|
exec-sh "^0.3.2"
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
|
||||||
"@expo/entity-cache-adapter-local-memory@file:packages/entity-cache-adapter-local-memory":
|
|
||||||
version "0.23.0"
|
|
||||||
dependencies:
|
|
||||||
lru-cache "^7.3.1"
|
|
||||||
|
|
||||||
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
|
"@expo/entity-cache-adapter-redis@file:packages/entity-cache-adapter-redis":
|
||||||
version "0.23.0"
|
version "0.21.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
ioredis "^4.27.3"
|
ioredis "^4.27.3"
|
||||||
|
|
||||||
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
|
"@expo/entity-database-adapter-knex@file:packages/entity-database-adapter-knex":
|
||||||
version "0.23.0"
|
version "0.21.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
knex "^1.0.2"
|
knex "^1.0.2"
|
||||||
|
|
||||||
"@expo/entity-ip-address-field@file:packages/entity-ip-address-field":
|
"@expo/entity-ip-address-field@file:packages/entity-ip-address-field":
|
||||||
version "0.23.0"
|
version "0.21.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
ip-address "^8.1.0"
|
ip-address "^8.1.0"
|
||||||
|
|
||||||
"@expo/entity-secondary-cache-redis@file:packages/entity-secondary-cache-redis":
|
"@expo/entity-secondary-cache-redis@file:packages/entity-secondary-cache-redis":
|
||||||
version "0.23.0"
|
version "0.21.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
ioredis "^4.17.3"
|
ioredis "^4.17.3"
|
||||||
|
|
||||||
"@expo/entity@file:packages/entity":
|
"@expo/entity@file:packages/entity":
|
||||||
version "0.23.0"
|
version "0.21.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@expo/results" "^1.0.0"
|
"@expo/results" "^1.0.0"
|
||||||
dataloader "^2.0.0"
|
dataloader "^2.0.0"
|
||||||
@@ -2000,11 +1995,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||||
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
||||||
|
|
||||||
"@types/lru-cache@^5.1.1":
|
|
||||||
version "5.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef"
|
|
||||||
integrity sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==
|
|
||||||
|
|
||||||
"@types/mime@*":
|
"@types/mime@*":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
|
||||||
@@ -6575,11 +6565,6 @@ lru-cache@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
lru-cache@^7.3.1:
|
|
||||||
version "7.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.3.1.tgz#7702e80694ec2bf19865567a469f2b081fcf53f5"
|
|
||||||
integrity sha512-nX1x4qUrKqwbIAhv4s9et4FIUVzNOpeY07bsjGUy8gwJrXH/wScImSQqXErmo/b2jZY2r0mohbLA9zVj7u1cNw==
|
|
||||||
|
|
||||||
lunr@^2.3.9:
|
lunr@^2.3.9:
|
||||||
version "2.3.9"
|
version "2.3.9"
|
||||||
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
|
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
|
||||||
|
|||||||
Reference in new issue
Block a user