@expo/entity-cache-adapter-local-memory 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +44 -0
  2. package/build/GenericLocalMemoryCacher.d.ts +20 -0
  3. package/build/GenericLocalMemoryCacher.js +79 -0
  4. package/build/GenericLocalMemoryCacher.js.map +1 -0
  5. package/build/LocalMemoryCacheAdapter.d.ts +11 -0
  6. package/build/LocalMemoryCacheAdapter.js +45 -0
  7. package/build/LocalMemoryCacheAdapter.js.map +1 -0
  8. package/build/LocalMemoryCacheAdapterProvider.d.ts +13 -0
  9. package/build/LocalMemoryCacheAdapterProvider.js +30 -0
  10. package/build/LocalMemoryCacheAdapterProvider.js.map +1 -0
  11. package/build/__integration-tests__/LocalMemoryCacheAdapter-integration-test.d.ts +1 -0
  12. package/build/__integration-tests__/LocalMemoryCacheAdapter-integration-test.js +128 -0
  13. package/build/__integration-tests__/LocalMemoryCacheAdapter-integration-test.js.map +1 -0
  14. package/build/__tests__/LocalMemoryCacheAdapter-test.d.ts +1 -0
  15. package/build/__tests__/LocalMemoryCacheAdapter-test.js +118 -0
  16. package/build/__tests__/LocalMemoryCacheAdapter-test.js.map +1 -0
  17. package/build/testfixtures/LocalMemoryTestEntity.d.ts +16 -0
  18. package/build/testfixtures/LocalMemoryTestEntity.js +53 -0
  19. package/build/testfixtures/LocalMemoryTestEntity.js.map +1 -0
  20. package/build/testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider.d.ts +6 -0
  21. package/build/testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider.js +32 -0
  22. package/build/testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider.js.map +1 -0
  23. package/package.json +38 -0
  24. package/src/GenericLocalMemoryCacher.ts +154 -0
  25. package/src/LocalMemoryCacheAdapter.ts +80 -0
  26. package/src/LocalMemoryCacheAdapterProvider.ts +43 -0
  27. package/src/__integration-tests__/LocalMemoryCacheAdapter-integration-test.ts +180 -0
  28. package/src/__tests__/LocalMemoryCacheAdapter-test.ts +125 -0
  29. package/src/testfixtures/LocalMemoryTestEntity.ts +100 -0
  30. package/src/testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider.ts +48 -0
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNoopLocalMemoryIntegrationTestEntityCompanionProvider = exports.createLocalMemoryIntegrationTestEntityCompanionProvider = void 0;
4
+ const entity_1 = require("@expo/entity");
5
+ const LocalMemoryCacheAdapterProvider_1 = require("../LocalMemoryCacheAdapterProvider");
6
+ const createLocalMemoryIntegrationTestEntityCompanionProvider = (localMemoryOptions = {}, metricsAdapter = new entity_1.NoOpEntityMetricsAdapter()) => {
7
+ const localMemoryCacheAdapterProvider = localMemoryOptions.maxSize === 0 && localMemoryOptions.ttlSeconds === 0
8
+ ? LocalMemoryCacheAdapterProvider_1.LocalMemoryCacheAdapterProvider.getNoOpProvider()
9
+ : LocalMemoryCacheAdapterProvider_1.LocalMemoryCacheAdapterProvider.getProvider(localMemoryOptions);
10
+ return new entity_1.EntityCompanionProvider(metricsAdapter, new Map([
11
+ [
12
+ 'postgres',
13
+ {
14
+ adapterProvider: new entity_1.StubDatabaseAdapterProvider(),
15
+ queryContextProvider: entity_1.StubQueryContextProvider,
16
+ },
17
+ ],
18
+ ]), new Map([
19
+ [
20
+ 'local-memory',
21
+ {
22
+ cacheAdapterProvider: localMemoryCacheAdapterProvider,
23
+ },
24
+ ],
25
+ ]));
26
+ };
27
+ exports.createLocalMemoryIntegrationTestEntityCompanionProvider = createLocalMemoryIntegrationTestEntityCompanionProvider;
28
+ const createNoopLocalMemoryIntegrationTestEntityCompanionProvider = (metricsAdapter = new entity_1.NoOpEntityMetricsAdapter()) => {
29
+ return (0, exports.createLocalMemoryIntegrationTestEntityCompanionProvider)({ maxSize: 0, ttlSeconds: 0 }, metricsAdapter);
30
+ };
31
+ exports.createNoopLocalMemoryIntegrationTestEntityCompanionProvider = createNoopLocalMemoryIntegrationTestEntityCompanionProvider;
32
+ //# sourceMappingURL=createLocalMemoryIntegrationTestEntityCompanionProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createLocalMemoryIntegrationTestEntityCompanionProvider.js","sourceRoot":"","sources":["../../src/testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider.ts"],"names":[],"mappings":";;;AAAA,yCAMsB;AAEtB,wFAAqF;AAE9E,MAAM,uDAAuD,GAAG,CACrE,qBAAgE,EAAE,EAClE,iBAAwC,IAAI,iCAAwB,EAAE,EAC7C,EAAE;IAC3B,MAAM,+BAA+B,GACnC,kBAAkB,CAAC,OAAO,KAAK,CAAC,IAAI,kBAAkB,CAAC,UAAU,KAAK,CAAC;QACrE,CAAC,CAAC,iEAA+B,CAAC,eAAe,EAAE;QACnD,CAAC,CAAC,iEAA+B,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IACtE,OAAO,IAAI,gCAAuB,CAChC,cAAc,EACd,IAAI,GAAG,CAAC;QACN;YACE,UAAU;YACV;gBACE,eAAe,EAAE,IAAI,oCAA2B,EAAE;gBAClD,oBAAoB,EAAE,iCAAwB;aAC/C;SACF;KACF,CAAC,EACF,IAAI,GAAG,CAAC;QACN;YACE,cAAc;YACd;gBACE,oBAAoB,EAAE,+BAA+B;aACtD;SACF;KACF,CAAC,CACH,CAAC;AACJ,CAAC,CAAC;AA5BW,QAAA,uDAAuD,2DA4BlE;AAEK,MAAM,2DAA2D,GAAG,CACzE,iBAAwC,IAAI,iCAAwB,EAAE,EAC7C,EAAE;IAC3B,OAAO,IAAA,+DAAuD,EAC5D,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,EAC7B,cAAc,CACf,CAAC;AACJ,CAAC,CAAC;AAPW,QAAA,2DAA2D,+DAOtE"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@expo/entity-cache-adapter-local-memory",
3
+ "version": "0.24.0",
4
+ "description": "Cross-request local memory cache adapter for @expo/entity",
5
+ "files": [
6
+ "build",
7
+ "src"
8
+ ],
9
+ "main": "build/index.js",
10
+ "types": "build/index.d.ts",
11
+ "scripts": {
12
+ "tsc": "tsc",
13
+ "clean": "rm -rf build coverage coverage-integration",
14
+ "lint": "eslint src",
15
+ "lint-fix": "eslint src --fix",
16
+ "test": "jest --rootDir . --config ../../resources/jest.config.js",
17
+ "integration": "../../resources/run-with-docker yarn integration-no-setup",
18
+ "integration-no-setup": "jest --config ../../resources/jest-integration.config.js --rootDir . --runInBand --passWithNoTests",
19
+ "barrelsby": "barrelsby --directory src --location top --exclude tests__ --singleQuotes --exportDefault --delete"
20
+ },
21
+ "engines": {
22
+ "node": ">=12"
23
+ },
24
+ "keywords": [
25
+ "entity"
26
+ ],
27
+ "author": "Expo",
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@expo/entity": "*"
31
+ },
32
+ "dependencies": {
33
+ "lru-cache": "^7.3.1"
34
+ },
35
+ "devDependencies": {
36
+ "@expo/entity": "^0.24.0"
37
+ }
38
+ }
@@ -0,0 +1,154 @@
1
+ import { CacheLoadResult, CacheStatus } from '@expo/entity';
2
+ import LRUCache from 'lru-cache';
3
+
4
+ // Sentinel value we store in local memory to negatively cache a database miss.
5
+ // The sentinel value is distinct from any (positively) cached value.
6
+ export const DOES_NOT_EXIST_LOCAL_MEMORY_CACHE = Symbol('doesNotExist');
7
+ type LocalMemoryCacheValue<TFields> = Readonly<TFields> | typeof DOES_NOT_EXIST_LOCAL_MEMORY_CACHE;
8
+ export type LocalMemoryCache<TFields> = LRUCache<string, LocalMemoryCacheValue<TFields>>;
9
+
10
+ type LRUCacheOptionsV7<K, V> = {
11
+ /**
12
+ * the number of most recently used items to keep.
13
+ * note that we may store fewer items than this if maxSize is hit.
14
+ */
15
+ max: number;
16
+
17
+ /**
18
+ * if you wish to track item size, you must provide a maxSize
19
+ * note that we still will only keep up to max *actual items*,
20
+ * so size tracking may cause fewer than max items to be stored.
21
+ * At the extreme, a single item of maxSize size will cause everything
22
+ * else in the cache to be dropped when it is added. Use with caution!
23
+ * Note also that size tracking can negatively impact performance,
24
+ * though for most cases, only minimally.
25
+ */
26
+
27
+ maxSize?: number;
28
+
29
+ /**
30
+ * function to calculate size of items. useful if storing strings or
31
+ * buffers or other items where memory size depends on the object itself.
32
+ * also note that oversized items do NOT immediately get dropped from
33
+ * the cache, though they will cause faster turnover in the storage.
34
+ */
35
+ sizeCalculation?: (value: V, key: K) => number;
36
+
37
+ /**
38
+ * function to call when the item is removed from the cache
39
+ * Note that using this can negatively impact performance.
40
+ */
41
+ dispose?: (value: V, key: K) => void;
42
+
43
+ /**
44
+ * max time to live for items before they are considered stale
45
+ * note that stale items are NOT preemptively removed by default,
46
+ * and MAY live in the cache, contributing to its LRU max, long after
47
+ * they have expired.
48
+ * Also, as this cache is optimized for LRU/MRU operations, some of
49
+ * the staleness/TTL checks will reduce performance, as they will incur
50
+ * overhead by deleting items.
51
+ * Must be a positive integer in ms, defaults to 0, which means "no TTL"
52
+ */
53
+ ttl?: number;
54
+
55
+ /**
56
+ * return stale items from cache.get() before disposing of them
57
+ * boolean, default false
58
+ */
59
+ allowStale?: boolean;
60
+
61
+ /**
62
+ * update the age of items on cache.get(), renewing their TTL
63
+ * boolean, default false
64
+ */
65
+ updateAgeOnGet?: boolean;
66
+
67
+ /**
68
+ * update the age of items on cache.has(), renewing their TTL
69
+ * boolean, default false
70
+ */
71
+ updateAgeOnHas?: boolean;
72
+
73
+ /**
74
+ * update the "recently-used"-ness of items on cache.has()
75
+ * boolean, default false
76
+ */
77
+ updateRecencyOnHas?: boolean;
78
+ };
79
+
80
+ export default class GenericLocalMemoryCacher<TFields> {
81
+ constructor(private readonly lruCache: LocalMemoryCache<TFields>) {}
82
+
83
+ static createLRUCache<TFields>(
84
+ options: { maxSize?: number; ttlSeconds?: number } = {}
85
+ ): LocalMemoryCache<TFields> {
86
+ const DEFAULT_LRU_CACHE_MAX_AGE_SECONDS = 10;
87
+ const DEFAULT_LRU_CACHE_SIZE = 10000;
88
+ const maxAgeSeconds = options.ttlSeconds ?? DEFAULT_LRU_CACHE_MAX_AGE_SECONDS;
89
+ const lruCacheOptions: LRUCacheOptionsV7<string, TFields> = {
90
+ max: options.maxSize ?? DEFAULT_LRU_CACHE_SIZE,
91
+ sizeCalculation: (value: LocalMemoryCacheValue<TFields>) =>
92
+ value === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE ? 0 : 1,
93
+ ttl: maxAgeSeconds * 1000, // convert to ms
94
+ };
95
+ return new LRUCache<string, LocalMemoryCacheValue<TFields>>(lruCacheOptions as any);
96
+ }
97
+
98
+ static createNoOpLRUCache<TFields>(): LocalMemoryCache<TFields> {
99
+ return new LRUCache<string, LocalMemoryCacheValue<TFields>>({
100
+ max: 0,
101
+ maxAge: -1,
102
+ });
103
+ }
104
+
105
+ public async loadManyAsync(
106
+ keys: readonly string[]
107
+ ): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>> {
108
+ const cacheResults = new Map<string, CacheLoadResult<TFields>>();
109
+ for (const key of keys) {
110
+ const cacheResult = this.lruCache.get(key);
111
+ if (cacheResult === DOES_NOT_EXIST_LOCAL_MEMORY_CACHE) {
112
+ cacheResults.set(key, {
113
+ status: CacheStatus.NEGATIVE,
114
+ });
115
+ } else if (cacheResult) {
116
+ cacheResults.set(key, {
117
+ status: CacheStatus.HIT,
118
+ item: cacheResult as unknown as TFields,
119
+ });
120
+ } else {
121
+ cacheResults.set(key, {
122
+ status: CacheStatus.MISS,
123
+ });
124
+ }
125
+ }
126
+ return cacheResults;
127
+ }
128
+
129
+ public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void> {
130
+ for (const [key, item] of objectMap) {
131
+ this.lruCache.set(key, item);
132
+ }
133
+ }
134
+
135
+ public async cacheDBMissesAsync(keys: string[]): Promise<void> {
136
+ for (const key of keys) {
137
+ this.lruCache.set(key, DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
138
+ }
139
+ }
140
+
141
+ public async invalidateManyAsync(keys: string[]): Promise<void> {
142
+ for (const key of keys) {
143
+ this.lruCache.del(key);
144
+ }
145
+ }
146
+
147
+ public makeCacheKey(parts: string[]): string {
148
+ const delimiter = ':';
149
+ const escapedParts = parts.map((part) =>
150
+ part.replace('\\', '\\\\').replace(delimiter, `\\${delimiter}`)
151
+ );
152
+ return escapedParts.join(delimiter);
153
+ }
154
+ }
@@ -0,0 +1,80 @@
1
+ import { EntityCacheAdapter, CacheLoadResult, EntityConfiguration, mapKeys } from '@expo/entity';
2
+ import invariant from 'invariant';
3
+
4
+ import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
5
+
6
+ export default class LocalMemoryCacheAdapter<TFields> extends EntityCacheAdapter<TFields> {
7
+ private readonly genericLocalMemoryCacher: GenericLocalMemoryCacher<TFields>;
8
+
9
+ constructor(
10
+ entityConfiguration: EntityConfiguration<TFields>,
11
+ lruCache: LocalMemoryCache<TFields>
12
+ ) {
13
+ super(entityConfiguration);
14
+ this.genericLocalMemoryCacher = new GenericLocalMemoryCacher(lruCache);
15
+ }
16
+
17
+ public async loadManyAsync<N extends keyof TFields>(
18
+ fieldName: N,
19
+ fieldValues: readonly NonNullable<TFields[N]>[]
20
+ ): Promise<ReadonlyMap<NonNullable<TFields[N]>, CacheLoadResult<TFields>>> {
21
+ const localMemoryCacheKeyToFieldValueMapping = new Map(
22
+ fieldValues.map((fieldValue) => [this.makeCacheKey(fieldName, fieldValue), fieldValue])
23
+ );
24
+ const cacheResults = await this.genericLocalMemoryCacher.loadManyAsync(
25
+ Array.from(localMemoryCacheKeyToFieldValueMapping.keys())
26
+ );
27
+
28
+ return mapKeys(cacheResults, (cacheKey) => {
29
+ const fieldValue = localMemoryCacheKeyToFieldValueMapping.get(cacheKey);
30
+ invariant(
31
+ fieldValue !== undefined,
32
+ 'Unspecified cache key %s returned from generic Local Memory cacher',
33
+ cacheKey
34
+ );
35
+ return fieldValue;
36
+ });
37
+ }
38
+
39
+ public async cacheManyAsync<N extends keyof TFields>(
40
+ fieldName: N,
41
+ objectMap: ReadonlyMap<NonNullable<TFields[N]>, Readonly<TFields>>
42
+ ): Promise<void> {
43
+ await this.genericLocalMemoryCacher.cacheManyAsync(
44
+ mapKeys(objectMap, (fieldValue) => this.makeCacheKey(fieldName, fieldValue))
45
+ );
46
+ }
47
+
48
+ public async cacheDBMissesAsync<N extends keyof TFields>(
49
+ fieldName: N,
50
+ fieldValues: readonly NonNullable<TFields[N]>[]
51
+ ): Promise<void> {
52
+ await this.genericLocalMemoryCacher.cacheDBMissesAsync(
53
+ fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
54
+ );
55
+ }
56
+
57
+ public async invalidateManyAsync<N extends keyof TFields>(
58
+ fieldName: N,
59
+ fieldValues: readonly NonNullable<TFields[N]>[]
60
+ ): Promise<void> {
61
+ await this.genericLocalMemoryCacher.invalidateManyAsync(
62
+ fieldValues.map((fieldValue) => this.makeCacheKey(fieldName, fieldValue))
63
+ );
64
+ }
65
+
66
+ private makeCacheKey<N extends keyof TFields>(
67
+ fieldName: N,
68
+ fieldValue: NonNullable<TFields[N]>
69
+ ): string {
70
+ const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
71
+ invariant(columnName, `database field mapping missing for ${fieldName}`);
72
+ const parts = [
73
+ this.entityConfiguration.tableName,
74
+ `${this.entityConfiguration.cacheKeyVersion}`,
75
+ columnName,
76
+ String(fieldValue),
77
+ ];
78
+ return this.genericLocalMemoryCacher.makeCacheKey(parts);
79
+ }
80
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ computeIfAbsent,
3
+ EntityCacheAdapter,
4
+ EntityConfiguration,
5
+ IEntityCacheAdapterProvider,
6
+ } from '@expo/entity';
7
+
8
+ import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
9
+ import LocalMemoryCacheAdapter from './LocalMemoryCacheAdapter';
10
+
11
+ export class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
12
+ // local memory cache adapters should be shared/reused across requests
13
+ static localMemoryCacheAdapterMap = new Map<string, LocalMemoryCacheAdapter<any>>();
14
+
15
+ static getNoOpProvider(): IEntityCacheAdapterProvider {
16
+ return new LocalMemoryCacheAdapterProvider(<TFields>() =>
17
+ GenericLocalMemoryCacher.createNoOpLRUCache<TFields>()
18
+ );
19
+ }
20
+
21
+ static getProvider(
22
+ options: { maxSize?: number; ttlSeconds?: number } = {}
23
+ ): IEntityCacheAdapterProvider {
24
+ return new LocalMemoryCacheAdapterProvider(<TFields>() =>
25
+ GenericLocalMemoryCacher.createLRUCache<TFields>(options)
26
+ );
27
+ }
28
+
29
+ private constructor(private readonly lruCacheCreator: <TFields>() => LocalMemoryCache<TFields>) {}
30
+
31
+ public getCacheAdapter<TFields>(
32
+ entityConfiguration: EntityConfiguration<TFields>
33
+ ): EntityCacheAdapter<TFields> {
34
+ return computeIfAbsent(
35
+ LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap,
36
+ entityConfiguration.tableName,
37
+ () => {
38
+ const lruCache = this.lruCacheCreator<TFields>();
39
+ return new LocalMemoryCacheAdapter(entityConfiguration, lruCache);
40
+ }
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,180 @@
1
+ import { CacheStatus, ViewerContext } from '@expo/entity';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ import GenericLocalMemoryCacher from '../GenericLocalMemoryCacher';
5
+ import LocalMemoryCacheAdapter from '../LocalMemoryCacheAdapter';
6
+ import { LocalMemoryCacheAdapterProvider } from '../LocalMemoryCacheAdapterProvider';
7
+ import LocalMemoryTestEntity from '../testfixtures/LocalMemoryTestEntity';
8
+ import {
9
+ createLocalMemoryIntegrationTestEntityCompanionProvider,
10
+ createNoopLocalMemoryIntegrationTestEntityCompanionProvider,
11
+ } from '../testfixtures/createLocalMemoryIntegrationTestEntityCompanionProvider';
12
+
13
+ class TestViewerContext extends ViewerContext {}
14
+
15
+ describe(LocalMemoryCacheAdapter, () => {
16
+ beforeEach(async () => {
17
+ LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.clear();
18
+ });
19
+
20
+ it('has correct caching behavior', async () => {
21
+ const viewerContext = new TestViewerContext(
22
+ createLocalMemoryIntegrationTestEntityCompanionProvider()
23
+ );
24
+ const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
25
+ LocalMemoryTestEntity,
26
+ LocalMemoryTestEntity.getCompanionDefinition()
27
+ )['tableDataCoordinator']['cacheAdapter'];
28
+ const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
29
+
30
+ const date = new Date();
31
+ const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
32
+ .setField('name', 'blah')
33
+ .setField('dateField', date)
34
+ .enforceCreateAsync();
35
+
36
+ // loading an entity should put it in cache
37
+ const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
38
+ .enforcing()
39
+ .loadByIDAsync(entity1Created.getID());
40
+
41
+ const entitySpecificGenericCacher =
42
+ LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
43
+ LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
44
+ )!['genericLocalMemoryCacher'];
45
+ const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
46
+ cacheKeyMaker('id', entity1.getID()),
47
+ ]);
48
+ const cachedValue = cachedResult.get(cacheKeyMaker('id', entity1.getID()))!;
49
+ expect(cachedValue).toMatchObject({
50
+ status: CacheStatus.HIT,
51
+ item: {
52
+ id: entity1.getID(),
53
+ name: 'blah',
54
+ dateField: date,
55
+ },
56
+ });
57
+
58
+ // simulate non existent db fetch, should write negative result ('') to cache
59
+ const nonExistentId = uuidv4();
60
+
61
+ const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
62
+ nonExistentId
63
+ );
64
+ expect(entityNonExistentResult.ok).toBe(false);
65
+
66
+ const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
67
+ cacheKeyMaker('id', nonExistentId),
68
+ ]);
69
+ expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
70
+ status: CacheStatus.NEGATIVE,
71
+ });
72
+
73
+ // load again through entities framework to ensure it reads negative result
74
+ const entityNonExistentResult2 = await LocalMemoryTestEntity.loader(
75
+ viewerContext
76
+ ).loadByIDAsync(nonExistentId);
77
+ expect(entityNonExistentResult2.ok).toBe(false);
78
+
79
+ // invalidate from cache to ensure it invalidates correctly
80
+ await LocalMemoryTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
81
+ const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync([
82
+ cacheKeyMaker('id', entity1.getID()),
83
+ ]);
84
+ const cachedValueMiss = cachedResultMiss.get(cacheKeyMaker('id', entity1.getID()));
85
+ expect(cachedValueMiss).toMatchObject({ status: CacheStatus.MISS });
86
+ });
87
+
88
+ it('shares the cache between different requests', async () => {
89
+ const genericLocalMemoryCacherLoadManySpy = jest.spyOn(
90
+ GenericLocalMemoryCacher.prototype as unknown as any,
91
+ 'loadManyAsync'
92
+ );
93
+ const viewerContext = new TestViewerContext(
94
+ createLocalMemoryIntegrationTestEntityCompanionProvider()
95
+ );
96
+
97
+ const date = new Date();
98
+ const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
99
+ .setField('name', 'blah')
100
+ .setField('dateField', date)
101
+ .enforceCreateAsync();
102
+
103
+ // loading an entity should put it in cache
104
+ await LocalMemoryTestEntity.loader(viewerContext)
105
+ .enforcing()
106
+ .loadByIDAsync(entity1Created.getID());
107
+
108
+ // load entity with a different request
109
+ const viewerContext2 = new TestViewerContext(
110
+ createLocalMemoryIntegrationTestEntityCompanionProvider()
111
+ );
112
+ const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2)
113
+ .enforcing()
114
+ .loadByIDAsync(entity1Created.getID());
115
+
116
+ const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
117
+ LocalMemoryTestEntity,
118
+ LocalMemoryTestEntity.getCompanionDefinition()
119
+ )['tableDataCoordinator']['cacheAdapter'];
120
+ const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
121
+ expect(entity1WithVc2.getAllFields()).toMatchObject({
122
+ id: entity1WithVc2.getID(),
123
+ name: 'blah',
124
+ dateField: date,
125
+ });
126
+ expect(genericLocalMemoryCacherLoadManySpy).toBeCalledWith([
127
+ cacheKeyMaker('id', entity1WithVc2.getID()),
128
+ ]);
129
+ expect(genericLocalMemoryCacherLoadManySpy).toBeCalledTimes(2);
130
+ });
131
+
132
+ it('respects the parameters of a noop cache', async () => {
133
+ const viewerContext = new TestViewerContext(
134
+ createNoopLocalMemoryIntegrationTestEntityCompanionProvider()
135
+ );
136
+ const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
137
+ LocalMemoryTestEntity,
138
+ LocalMemoryTestEntity.getCompanionDefinition()
139
+ )['tableDataCoordinator']['cacheAdapter'];
140
+ const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);
141
+
142
+ const date = new Date();
143
+ const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
144
+ .setField('name', 'blah')
145
+ .setField('dateField', date)
146
+ .enforceCreateAsync();
147
+
148
+ // loading an entity will try to put it in cache but it's a noop cache, so it should be a miss
149
+ const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
150
+ .enforcing()
151
+ .loadByIDAsync(entity1Created.getID());
152
+
153
+ const entitySpecificGenericCacher =
154
+ LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
155
+ LocalMemoryTestEntity.getCompanionDefinition().entityConfiguration.tableName
156
+ )!['genericLocalMemoryCacher'];
157
+ const cachedResult = await entitySpecificGenericCacher.loadManyAsync([
158
+ cacheKeyMaker('id', entity1.getID()),
159
+ ]);
160
+ const cachedValue = cachedResult.get(cacheKeyMaker('id', entity1.getID()))!;
161
+ expect(cachedValue).toMatchObject({
162
+ status: CacheStatus.MISS,
163
+ });
164
+
165
+ // a non existent db fetch should try to write negative result ('') but it's a noop cache, so it should be a miss
166
+ const nonExistentId = uuidv4();
167
+
168
+ const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
169
+ nonExistentId
170
+ );
171
+ expect(entityNonExistentResult.ok).toBe(false);
172
+
173
+ const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
174
+ cacheKeyMaker('id', nonExistentId),
175
+ ]);
176
+ expect(nonExistentCachedResult.get(cacheKeyMaker('id', nonExistentId))).toMatchObject({
177
+ status: CacheStatus.MISS,
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,125 @@
1
+ import { CacheStatus, UUIDField, EntityConfiguration } from '@expo/entity';
2
+
3
+ import GenericLocalMemoryCacher, {
4
+ DOES_NOT_EXIST_LOCAL_MEMORY_CACHE,
5
+ } from '../GenericLocalMemoryCacher';
6
+ import LocalMemoryCacheAdapter from '../LocalMemoryCacheAdapter';
7
+
8
+ type BlahFields = {
9
+ id: string;
10
+ };
11
+
12
+ const entityConfiguration = new EntityConfiguration<BlahFields>({
13
+ idField: 'id',
14
+ tableName: 'blah',
15
+ schema: {
16
+ id: new UUIDField({ columnName: 'id', cache: true }),
17
+ },
18
+ databaseAdapterFlavor: 'postgres',
19
+ cacheAdapterFlavor: 'local-memory',
20
+ });
21
+
22
+ describe(LocalMemoryCacheAdapter, () => {
23
+ describe('loadManyAsync', () => {
24
+ it('returns appropriate cache results', async () => {
25
+ const cacheAdapter = new LocalMemoryCacheAdapter(
26
+ entityConfiguration,
27
+ GenericLocalMemoryCacher.createLRUCache({
28
+ maxSize: Number.MAX_SAFE_INTEGER,
29
+ ttlSeconds: Number.MAX_SAFE_INTEGER,
30
+ })
31
+ );
32
+
33
+ const cacheHits = new Map<string, Readonly<BlahFields>>([['test-id-1', { id: 'test-id-1' }]]);
34
+ await cacheAdapter.cacheManyAsync('id', cacheHits);
35
+ await cacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
36
+
37
+ const results = await cacheAdapter.loadManyAsync('id', [
38
+ 'test-id-1',
39
+ 'test-id-2',
40
+ 'test-id-3',
41
+ ]);
42
+
43
+ expect(results.get('test-id-1')).toMatchObject({
44
+ status: CacheStatus.HIT,
45
+ item: { id: 'test-id-1' },
46
+ });
47
+ expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.NEGATIVE });
48
+ expect(results.get('test-id-3')).toMatchObject({ status: CacheStatus.MISS });
49
+ expect(results.size).toBe(3);
50
+ });
51
+
52
+ it('returns empty map when passed empty array of fieldValues', async () => {
53
+ const cacheAdapter = new LocalMemoryCacheAdapter(
54
+ entityConfiguration,
55
+ GenericLocalMemoryCacher.createLRUCache({
56
+ maxSize: Number.MAX_SAFE_INTEGER,
57
+ ttlSeconds: Number.MAX_SAFE_INTEGER,
58
+ })
59
+ );
60
+ const results = await cacheAdapter.loadManyAsync('id', []);
61
+ expect(results).toEqual(new Map());
62
+ });
63
+ });
64
+
65
+ describe('cacheManyAsync', () => {
66
+ it('correctly caches all objects', async () => {
67
+ const lruCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
68
+ maxSize: Number.MAX_SAFE_INTEGER,
69
+ ttlSeconds: Number.MAX_SAFE_INTEGER,
70
+ });
71
+
72
+ const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, lruCache);
73
+ await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
74
+
75
+ const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
76
+ expect(lruCache.get(cacheKey)).toMatchObject({
77
+ id: 'test-id-1',
78
+ });
79
+ });
80
+ });
81
+
82
+ describe('cacheDBMissesAsync', () => {
83
+ it('correctly caches misses', async () => {
84
+ const lruCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
85
+ maxSize: Number.MAX_SAFE_INTEGER,
86
+ ttlSeconds: Number.MAX_SAFE_INTEGER,
87
+ });
88
+
89
+ const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, lruCache);
90
+ await cacheAdapter.cacheDBMissesAsync('id', ['test-id-1']);
91
+
92
+ const cacheKey = cacheAdapter['makeCacheKey']('id', 'test-id-1');
93
+ expect(lruCache.get(cacheKey)).toEqual(DOES_NOT_EXIST_LOCAL_MEMORY_CACHE);
94
+ });
95
+ });
96
+
97
+ describe('invalidateManyAsync', () => {
98
+ it('invalidates correctly', async () => {
99
+ const lruCache = GenericLocalMemoryCacher.createLRUCache<BlahFields>({
100
+ maxSize: Number.MAX_SAFE_INTEGER,
101
+ ttlSeconds: Number.MAX_SAFE_INTEGER,
102
+ });
103
+
104
+ const cacheAdapter = new LocalMemoryCacheAdapter(entityConfiguration, lruCache);
105
+ await cacheAdapter.cacheManyAsync('id', new Map([['test-id-1', { id: 'test-id-1' }]]));
106
+ await cacheAdapter.cacheDBMissesAsync('id', ['test-id-2']);
107
+ await cacheAdapter.invalidateManyAsync('id', ['test-id-1', 'test-id-2']);
108
+
109
+ const results = await cacheAdapter.loadManyAsync('id', ['test-id-1', 'test-id-2']);
110
+ expect(results.get('test-id-1')).toMatchObject({ status: CacheStatus.MISS });
111
+ expect(results.get('test-id-2')).toMatchObject({ status: CacheStatus.MISS });
112
+ });
113
+
114
+ it('returns when passed empty array of fieldValues', async () => {
115
+ const cacheAdapter = new LocalMemoryCacheAdapter(
116
+ entityConfiguration,
117
+ GenericLocalMemoryCacher.createLRUCache<BlahFields>({
118
+ maxSize: Number.MAX_SAFE_INTEGER,
119
+ ttlSeconds: Number.MAX_SAFE_INTEGER,
120
+ })
121
+ );
122
+ await cacheAdapter.invalidateManyAsync('id', []);
123
+ });
124
+ });
125
+ });