@expo/entity-cache-adapter-redis 0.40.0 → 0.42.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.
- package/build/GenericRedisCacher.d.ts +26 -4
- package/build/GenericRedisCacher.js +36 -5
- package/build/GenericRedisCacher.js.map +1 -1
- package/build/RedisCacheAdapterProvider.d.ts +1 -1
- package/build/RedisCacheAdapterProvider.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/tsconfig.build.tsbuildinfo +1 -0
- package/build/utils/getSurroundingCacheKeyVersionsForInvalidation.d.ts +1 -0
- package/build/utils/getSurroundingCacheKeyVersionsForInvalidation.js +11 -0
- package/build/utils/getSurroundingCacheKeyVersionsForInvalidation.js.map +1 -0
- package/package.json +9 -7
- package/src/GenericRedisCacher.ts +72 -13
- package/src/RedisCacheAdapterProvider.ts +3 -3
- package/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts +48 -29
- package/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts +104 -45
- package/src/__integration-tests__/GenericRedisCacher-integration-test.ts +7 -5
- package/src/__integration-tests__/errors-test.ts +8 -4
- package/src/{testfixtures → __testfixtures__}/RedisTestEntity.ts +13 -29
- package/src/{testfixtures → __testfixtures__}/createRedisIntegrationTestEntityCompanionProvider.ts +1 -2
- package/src/__tests__/GenericRedisCacher-test.ts +68 -12
- package/src/index.ts +1 -0
- package/src/utils/__tests__/getSurroundingCacheKeyVersionsForInvalidation-test.ts +9 -0
- package/src/utils/getSurroundingCacheKeyVersionsForInvalidation.ts +9 -0
- package/build/__integration-tests__/BatchedRedisCacheAdapter-integration-test.d.ts +0 -1
- package/build/__integration-tests__/BatchedRedisCacheAdapter-integration-test.js +0 -133
- package/build/__integration-tests__/BatchedRedisCacheAdapter-integration-test.js.map +0 -1
- package/build/__integration-tests__/GenericRedisCacher-full-integration-test.d.ts +0 -1
- package/build/__integration-tests__/GenericRedisCacher-full-integration-test.js +0 -110
- package/build/__integration-tests__/GenericRedisCacher-full-integration-test.js.map +0 -1
- package/build/__integration-tests__/GenericRedisCacher-integration-test.d.ts +0 -1
- package/build/__integration-tests__/GenericRedisCacher-integration-test.js +0 -124
- package/build/__integration-tests__/GenericRedisCacher-integration-test.js.map +0 -1
- package/build/__integration-tests__/errors-test.d.ts +0 -1
- package/build/__integration-tests__/errors-test.js +0 -39
- package/build/__integration-tests__/errors-test.js.map +0 -1
- package/build/__tests__/GenericRedisCacher-test.d.ts +0 -1
- package/build/__tests__/GenericRedisCacher-test.js +0 -141
- package/build/__tests__/GenericRedisCacher-test.js.map +0 -1
- package/build/errors/__tests__/wrapNativeRedisCallAsync-test.d.ts +0 -1
- package/build/errors/__tests__/wrapNativeRedisCallAsync-test.js +0 -42
- package/build/errors/__tests__/wrapNativeRedisCallAsync-test.js.map +0 -1
- package/build/testfixtures/RedisTestEntity.d.ts +0 -16
- package/build/testfixtures/RedisTestEntity.js +0 -49
- package/build/testfixtures/RedisTestEntity.js.map +0 -1
- package/build/testfixtures/createRedisIntegrationTestEntityCompanionProvider.d.ts +0 -3
- package/build/testfixtures/createRedisIntegrationTestEntityCompanionProvider.js +0 -31
- package/build/testfixtures/createRedisIntegrationTestEntityCompanionProvider.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CacheLoadResult, EntityConfiguration, IEntityGenericCacher } from '@expo/entity';
|
|
1
|
+
import { CacheLoadResult, EntityConfiguration, IEntityGenericCacher, IEntityLoadKey, IEntityLoadValue } from '@expo/entity';
|
|
2
2
|
export interface IRedisTransaction {
|
|
3
3
|
set(key: string, value: string, secondsToken: 'EX', seconds: number): this;
|
|
4
4
|
exec(): Promise<any>;
|
|
@@ -8,6 +8,22 @@ export interface IRedis {
|
|
|
8
8
|
multi(): IRedisTransaction;
|
|
9
9
|
del(...args: [...keys: string[]]): Promise<any>;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The strategy for generating the set of cache keys to invalidate in the Redis cache after entity mutation.
|
|
13
|
+
*/
|
|
14
|
+
export declare enum RedisCacheInvalidationStrategy {
|
|
15
|
+
/**
|
|
16
|
+
* Invalidate just the cache key(s) for the current cacheKeyVersion of the entity.
|
|
17
|
+
*/
|
|
18
|
+
CURRENT_CACHE_KEY_VERSION = "current-cache-key-version",
|
|
19
|
+
/**
|
|
20
|
+
* Invalidate the cache key(s) for the current cacheKeyVersion and the surrounding cache key versions
|
|
21
|
+
* (e.g. `1`, `2` and `3` if the current version is `2`). This can be useful for deployment safety, where
|
|
22
|
+
* some machines may be operating on an old version of the code and thus an old cacheKeyVersion, and some the new version.
|
|
23
|
+
* This strategy generates cache keys for both old and potential future new versions.
|
|
24
|
+
*/
|
|
25
|
+
SURROUNDING_CACHE_KEY_VERSIONS = "surrounding-cache-key-versions"
|
|
26
|
+
}
|
|
11
27
|
export interface GenericRedisCacheContext {
|
|
12
28
|
/**
|
|
13
29
|
* Instance of ioredis.Redis
|
|
@@ -33,14 +49,20 @@ export interface GenericRedisCacheContext {
|
|
|
33
49
|
* this TTL will be assumed not present in the database (unless invalidated).
|
|
34
50
|
*/
|
|
35
51
|
ttlSecondsNegative: number;
|
|
52
|
+
/**
|
|
53
|
+
* Invalidation strategy for the cache.
|
|
54
|
+
*/
|
|
55
|
+
invalidationStrategy: RedisCacheInvalidationStrategy;
|
|
36
56
|
}
|
|
37
|
-
export default class GenericRedisCacher<TFields extends Record<string, any
|
|
57
|
+
export default class GenericRedisCacher<TFields extends Record<string, any>, TIDField extends keyof TFields> implements IEntityGenericCacher<TFields, TIDField> {
|
|
38
58
|
private readonly context;
|
|
39
59
|
private readonly entityConfiguration;
|
|
40
|
-
constructor(context: GenericRedisCacheContext, entityConfiguration: EntityConfiguration<TFields>);
|
|
60
|
+
constructor(context: GenericRedisCacheContext, entityConfiguration: EntityConfiguration<TFields, TIDField>);
|
|
41
61
|
loadManyAsync(keys: readonly string[]): Promise<ReadonlyMap<string, CacheLoadResult<TFields>>>;
|
|
42
62
|
cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TFields>>): Promise<void>;
|
|
43
63
|
cacheDBMissesAsync(keys: readonly string[]): Promise<void>;
|
|
44
64
|
invalidateManyAsync(keys: readonly string[]): Promise<void>;
|
|
45
|
-
|
|
65
|
+
private makeCacheKeyForCacheKeyVersion;
|
|
66
|
+
makeCacheKeyForStorage<TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>, TSerializedLoadValue, TLoadValue extends IEntityLoadValue<TSerializedLoadValue>>(key: TLoadKey, value: TLoadValue): string;
|
|
67
|
+
makeCacheKeysForInvalidation<TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>, TSerializedLoadValue, TLoadValue extends IEntityLoadValue<TSerializedLoadValue>>(key: TLoadKey, value: TLoadValue): readonly string[];
|
|
46
68
|
}
|
|
@@ -3,13 +3,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RedisCacheInvalidationStrategy = void 0;
|
|
6
7
|
const entity_1 = require("@expo/entity");
|
|
7
|
-
const invariant_1 = __importDefault(require("invariant"));
|
|
8
8
|
const RedisCommon_1 = require("./RedisCommon");
|
|
9
9
|
const wrapNativeRedisCallAsync_1 = __importDefault(require("./errors/wrapNativeRedisCallAsync"));
|
|
10
|
+
const getSurroundingCacheKeyVersionsForInvalidation_1 = require("./utils/getSurroundingCacheKeyVersionsForInvalidation");
|
|
10
11
|
// Sentinel value we store in Redis to negatively cache a database miss.
|
|
11
12
|
// The sentinel value is distinct from any (positively) cached value.
|
|
12
13
|
const DOES_NOT_EXIST_REDIS = '';
|
|
14
|
+
/**
|
|
15
|
+
* The strategy for generating the set of cache keys to invalidate in the Redis cache after entity mutation.
|
|
16
|
+
*/
|
|
17
|
+
var RedisCacheInvalidationStrategy;
|
|
18
|
+
(function (RedisCacheInvalidationStrategy) {
|
|
19
|
+
/**
|
|
20
|
+
* Invalidate just the cache key(s) for the current cacheKeyVersion of the entity.
|
|
21
|
+
*/
|
|
22
|
+
RedisCacheInvalidationStrategy["CURRENT_CACHE_KEY_VERSION"] = "current-cache-key-version";
|
|
23
|
+
/**
|
|
24
|
+
* Invalidate the cache key(s) for the current cacheKeyVersion and the surrounding cache key versions
|
|
25
|
+
* (e.g. `1`, `2` and `3` if the current version is `2`). This can be useful for deployment safety, where
|
|
26
|
+
* some machines may be operating on an old version of the code and thus an old cacheKeyVersion, and some the new version.
|
|
27
|
+
* This strategy generates cache keys for both old and potential future new versions.
|
|
28
|
+
*/
|
|
29
|
+
RedisCacheInvalidationStrategy["SURROUNDING_CACHE_KEY_VERSIONS"] = "surrounding-cache-key-versions";
|
|
30
|
+
})(RedisCacheInvalidationStrategy || (exports.RedisCacheInvalidationStrategy = RedisCacheInvalidationStrategy = {}));
|
|
13
31
|
class GenericRedisCacher {
|
|
14
32
|
context;
|
|
15
33
|
entityConfiguration;
|
|
@@ -71,10 +89,23 @@ class GenericRedisCacher {
|
|
|
71
89
|
}
|
|
72
90
|
await (0, wrapNativeRedisCallAsync_1.default)(() => this.context.redisClient.del(...keys));
|
|
73
91
|
}
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
return this.context.makeKeyFn(this.context.cacheKeyPrefix, this.entityConfiguration.tableName, `v2.${
|
|
92
|
+
makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion) {
|
|
93
|
+
const cacheKeyType = key.getLoadMethodType();
|
|
94
|
+
const parts = key.createCacheKeyPartsForLoadValue(this.entityConfiguration, value);
|
|
95
|
+
return this.context.makeKeyFn(this.context.cacheKeyPrefix, cacheKeyType, this.entityConfiguration.tableName, `v2.${cacheKeyVersion}`, ...parts);
|
|
96
|
+
}
|
|
97
|
+
makeCacheKeyForStorage(key, value) {
|
|
98
|
+
return this.makeCacheKeyForCacheKeyVersion(key, value, this.entityConfiguration.cacheKeyVersion);
|
|
99
|
+
}
|
|
100
|
+
makeCacheKeysForInvalidation(key, value) {
|
|
101
|
+
switch (this.context.invalidationStrategy) {
|
|
102
|
+
case RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION:
|
|
103
|
+
return [
|
|
104
|
+
this.makeCacheKeyForCacheKeyVersion(key, value, this.entityConfiguration.cacheKeyVersion),
|
|
105
|
+
];
|
|
106
|
+
case RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS:
|
|
107
|
+
return (0, getSurroundingCacheKeyVersionsForInvalidation_1.getSurroundingCacheKeyVersionsForInvalidation)(this.entityConfiguration.cacheKeyVersion).map((cacheKeyVersion) => this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion));
|
|
108
|
+
}
|
|
78
109
|
}
|
|
79
110
|
}
|
|
80
111
|
exports.default = GenericRedisCacher;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GenericRedisCacher.js","sourceRoot":"","sources":["../src/GenericRedisCacher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GenericRedisCacher.js","sourceRoot":"","sources":["../src/GenericRedisCacher.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,5 +3,5 @@ import { GenericRedisCacheContext } from './GenericRedisCacher';
|
|
|
3
3
|
export default class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
|
4
4
|
private readonly context;
|
|
5
5
|
constructor(context: GenericRedisCacheContext);
|
|
6
|
-
getCacheAdapter<TFields extends Record<string, any
|
|
6
|
+
getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(entityConfiguration: EntityConfiguration<TFields, TIDField>): IEntityCacheAdapter<TFields, TIDField>;
|
|
7
7
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisCacheAdapterProvider.js","sourceRoot":"","sources":["../src/RedisCacheAdapterProvider.ts"],"names":[],"mappings":";;;;;AAAA,yCAKsB;AAEtB,8EAAoF;AAEpF,MAAqB,yBAAyB;IACf;IAA7B,YAA6B,OAAiC;QAAjC,YAAO,GAAP,OAAO,CAA0B;IAAG,CAAC;IAElE,eAAe,CACb,
|
|
1
|
+
{"version":3,"file":"RedisCacheAdapterProvider.js","sourceRoot":"","sources":["../src/RedisCacheAdapterProvider.ts"],"names":[],"mappings":";;;;;AAAA,yCAKsB;AAEtB,8EAAoF;AAEpF,MAAqB,yBAAyB;IACf;IAA7B,YAA6B,OAAiC;QAAjC,YAAO,GAAP,OAAO,CAA0B;IAAG,CAAC;IAElE,eAAe,CACb,mBAA2D;QAE3D,OAAO,IAAI,kCAAyB,CAAC,IAAI,4BAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAClG,CAAC;CACF;AARD,4CAQC"}
|
package/build/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export * from './GenericRedisCacher';
|
|
|
7
7
|
export { default as RedisCacheAdapterProvider } from './RedisCacheAdapterProvider';
|
|
8
8
|
export * from './RedisCommon';
|
|
9
9
|
export { default as wrapNativeRedisCallAsync } from './errors/wrapNativeRedisCallAsync';
|
|
10
|
+
export * from './utils/getSurroundingCacheKeyVersionsForInvalidation';
|
package/build/index.js
CHANGED
|
@@ -31,4 +31,5 @@ Object.defineProperty(exports, "RedisCacheAdapterProvider", { enumerable: true,
|
|
|
31
31
|
__exportStar(require("./RedisCommon"), exports);
|
|
32
32
|
var wrapNativeRedisCallAsync_1 = require("./errors/wrapNativeRedisCallAsync");
|
|
33
33
|
Object.defineProperty(exports, "wrapNativeRedisCallAsync", { enumerable: true, get: function () { return __importDefault(wrapNativeRedisCallAsync_1).default; } });
|
|
34
|
+
__exportStar(require("./utils/getSurroundingCacheKeyVersionsForInvalidation"), exports);
|
|
34
35
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,iCAAiC;AACjC;;;GAGG;;;;;;;;;;;;;;;;;;;;AAEH,2DAAqE;AAA5D,yIAAA,OAAO,OAAsB;AACtC,uDAAqC;AACrC,yEAAmF;AAA1E,uJAAA,OAAO,OAA6B;AAC7C,gDAA8B;AAC9B,8EAAwF;AAA/E,qJAAA,OAAO,OAA4B"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,iCAAiC;AACjC;;;GAGG;;;;;;;;;;;;;;;;;;;;AAEH,2DAAqE;AAA5D,yIAAA,OAAO,OAAsB;AACtC,uDAAqC;AACrC,yEAAmF;AAA1E,uJAAA,OAAO,OAA6B;AAC7C,gDAA8B;AAC9B,8EAAwF;AAA/E,qJAAA,OAAO,OAA4B;AAC5C,wFAAsE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/genericrediscacher.ts","../src/rediscacheadapterprovider.ts","../src/rediscommon.ts","../src/index.ts","../src/errors/wrapnativerediscallasync.ts","../src/utils/getsurroundingcachekeyversionsforinvalidation.ts"],"version":"5.8.3"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getSurroundingCacheKeyVersionsForInvalidation(cacheKeyVersion: number): readonly number[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSurroundingCacheKeyVersionsForInvalidation = getSurroundingCacheKeyVersionsForInvalidation;
|
|
4
|
+
function getSurroundingCacheKeyVersionsForInvalidation(cacheKeyVersion) {
|
|
5
|
+
return [
|
|
6
|
+
...(cacheKeyVersion === 0 ? [] : [cacheKeyVersion - 1]),
|
|
7
|
+
cacheKeyVersion,
|
|
8
|
+
cacheKeyVersion + 1,
|
|
9
|
+
];
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=getSurroundingCacheKeyVersionsForInvalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getSurroundingCacheKeyVersionsForInvalidation.js","sourceRoot":"","sources":["../../src/utils/getSurroundingCacheKeyVersionsForInvalidation.ts"],"names":[],"mappings":";;AAAA,sGAQC;AARD,SAAgB,6CAA6C,CAC3D,eAAuB;IAEvB,OAAO;QACL,GAAG,CAAC,eAAe,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC;QACvD,eAAe;QACf,eAAe,GAAG,CAAC;KACpB,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity-cache-adapter-redis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Redis cache adapter for @expo/entity",
|
|
5
5
|
"files": [
|
|
6
6
|
"build",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"types": "build/index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"tsc": "tsc",
|
|
13
|
+
"build": "tsc -b tsconfig.build.json",
|
|
13
14
|
"clean": "rm -rf build coverage coverage-integration",
|
|
14
15
|
"lint": "eslint src",
|
|
15
16
|
"lint-fix": "eslint src --fix",
|
|
@@ -27,26 +28,27 @@
|
|
|
27
28
|
"author": "Expo",
|
|
28
29
|
"license": "MIT",
|
|
29
30
|
"dependencies": {
|
|
30
|
-
"@expo/entity": "^0.
|
|
31
|
+
"@expo/entity": "^0.42.0"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"ioredis": ">=5"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@expo/batcher": "^1.0.0",
|
|
37
|
-
"@
|
|
38
|
+
"@expo/entity-testing-utils": "^0.42.0",
|
|
39
|
+
"@types/jest": "^29.5.14",
|
|
38
40
|
"@types/node": "^20.14.1",
|
|
39
41
|
"ctix": "^2.7.0",
|
|
40
42
|
"eslint": "^8.57.1",
|
|
41
43
|
"eslint-config-universe": "^14.0.0",
|
|
42
44
|
"eslint-plugin-tsdoc": "^0.3.0",
|
|
43
|
-
"ioredis": "^5.
|
|
45
|
+
"ioredis": "^5.6.0",
|
|
44
46
|
"jest": "^29.7.0",
|
|
45
47
|
"prettier": "^3.3.3",
|
|
46
48
|
"prettier-plugin-organize-imports": "^4.1.0",
|
|
47
|
-
"ts-jest": "^29.
|
|
49
|
+
"ts-jest": "^29.3.1",
|
|
48
50
|
"ts-mockito": "^2.6.1",
|
|
49
|
-
"typescript": "^5.
|
|
51
|
+
"typescript": "^5.8.3"
|
|
50
52
|
},
|
|
51
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "8414d96d948882735687da146e84913397cd8368"
|
|
52
54
|
}
|
|
@@ -5,11 +5,13 @@ import {
|
|
|
5
5
|
transformCacheObjectToFields,
|
|
6
6
|
transformFieldsToCacheObject,
|
|
7
7
|
IEntityGenericCacher,
|
|
8
|
+
IEntityLoadKey,
|
|
9
|
+
IEntityLoadValue,
|
|
8
10
|
} from '@expo/entity';
|
|
9
|
-
import invariant from 'invariant';
|
|
10
11
|
|
|
11
12
|
import { redisTransformerMap } from './RedisCommon';
|
|
12
13
|
import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync';
|
|
14
|
+
import { getSurroundingCacheKeyVersionsForInvalidation } from './utils/getSurroundingCacheKeyVersionsForInvalidation';
|
|
13
15
|
|
|
14
16
|
// Sentinel value we store in Redis to negatively cache a database miss.
|
|
15
17
|
// The sentinel value is distinct from any (positively) cached value.
|
|
@@ -26,6 +28,24 @@ export interface IRedis {
|
|
|
26
28
|
del(...args: [...keys: string[]]): Promise<any>;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The strategy for generating the set of cache keys to invalidate in the Redis cache after entity mutation.
|
|
33
|
+
*/
|
|
34
|
+
export enum RedisCacheInvalidationStrategy {
|
|
35
|
+
/**
|
|
36
|
+
* Invalidate just the cache key(s) for the current cacheKeyVersion of the entity.
|
|
37
|
+
*/
|
|
38
|
+
CURRENT_CACHE_KEY_VERSION = 'current-cache-key-version',
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Invalidate the cache key(s) for the current cacheKeyVersion and the surrounding cache key versions
|
|
42
|
+
* (e.g. `1`, `2` and `3` if the current version is `2`). This can be useful for deployment safety, where
|
|
43
|
+
* some machines may be operating on an old version of the code and thus an old cacheKeyVersion, and some the new version.
|
|
44
|
+
* This strategy generates cache keys for both old and potential future new versions.
|
|
45
|
+
*/
|
|
46
|
+
SURROUNDING_CACHE_KEY_VERSIONS = 'surrounding-cache-key-versions',
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
export interface GenericRedisCacheContext {
|
|
30
50
|
/**
|
|
31
51
|
* Instance of ioredis.Redis
|
|
@@ -55,14 +75,21 @@ export interface GenericRedisCacheContext {
|
|
|
55
75
|
* this TTL will be assumed not present in the database (unless invalidated).
|
|
56
76
|
*/
|
|
57
77
|
ttlSecondsNegative: number;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Invalidation strategy for the cache.
|
|
81
|
+
*/
|
|
82
|
+
invalidationStrategy: RedisCacheInvalidationStrategy;
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
export default class GenericRedisCacher<
|
|
61
|
-
|
|
85
|
+
export default class GenericRedisCacher<
|
|
86
|
+
TFields extends Record<string, any>,
|
|
87
|
+
TIDField extends keyof TFields,
|
|
88
|
+
> implements IEntityGenericCacher<TFields, TIDField>
|
|
62
89
|
{
|
|
63
90
|
constructor(
|
|
64
91
|
private readonly context: GenericRedisCacheContext,
|
|
65
|
-
private readonly entityConfiguration: EntityConfiguration<TFields>,
|
|
92
|
+
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
66
93
|
) {}
|
|
67
94
|
|
|
68
95
|
public async loadManyAsync(
|
|
@@ -147,18 +174,50 @@ export default class GenericRedisCacher<TFields extends Record<string, any>>
|
|
|
147
174
|
await wrapNativeRedisCallAsync(() => this.context.redisClient.del(...keys));
|
|
148
175
|
}
|
|
149
176
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
private makeCacheKeyForCacheKeyVersion<
|
|
178
|
+
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
|
|
179
|
+
TSerializedLoadValue,
|
|
180
|
+
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
|
|
181
|
+
>(key: TLoadKey, value: TLoadValue, cacheKeyVersion: number): string {
|
|
182
|
+
const cacheKeyType = key.getLoadMethodType();
|
|
183
|
+
const parts = key.createCacheKeyPartsForLoadValue(this.entityConfiguration, value);
|
|
156
184
|
return this.context.makeKeyFn(
|
|
157
185
|
this.context.cacheKeyPrefix,
|
|
186
|
+
cacheKeyType,
|
|
158
187
|
this.entityConfiguration.tableName,
|
|
159
|
-
`v2.${
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
`v2.${cacheKeyVersion}`,
|
|
189
|
+
...parts,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public makeCacheKeyForStorage<
|
|
194
|
+
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
|
|
195
|
+
TSerializedLoadValue,
|
|
196
|
+
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
|
|
197
|
+
>(key: TLoadKey, value: TLoadValue): string {
|
|
198
|
+
return this.makeCacheKeyForCacheKeyVersion(
|
|
199
|
+
key,
|
|
200
|
+
value,
|
|
201
|
+
this.entityConfiguration.cacheKeyVersion,
|
|
162
202
|
);
|
|
163
203
|
}
|
|
204
|
+
|
|
205
|
+
public makeCacheKeysForInvalidation<
|
|
206
|
+
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
|
|
207
|
+
TSerializedLoadValue,
|
|
208
|
+
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
|
|
209
|
+
>(key: TLoadKey, value: TLoadValue): readonly string[] {
|
|
210
|
+
switch (this.context.invalidationStrategy) {
|
|
211
|
+
case RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION:
|
|
212
|
+
return [
|
|
213
|
+
this.makeCacheKeyForCacheKeyVersion(key, value, this.entityConfiguration.cacheKeyVersion),
|
|
214
|
+
];
|
|
215
|
+
case RedisCacheInvalidationStrategy.SURROUNDING_CACHE_KEY_VERSIONS:
|
|
216
|
+
return getSurroundingCacheKeyVersionsForInvalidation(
|
|
217
|
+
this.entityConfiguration.cacheKeyVersion,
|
|
218
|
+
).map((cacheKeyVersion) =>
|
|
219
|
+
this.makeCacheKeyForCacheKeyVersion(key, value, cacheKeyVersion),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
164
223
|
}
|
|
@@ -10,9 +10,9 @@ import GenericRedisCacher, { GenericRedisCacheContext } from './GenericRedisCach
|
|
|
10
10
|
export default class RedisCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
|
11
11
|
constructor(private readonly context: GenericRedisCacheContext) {}
|
|
12
12
|
|
|
13
|
-
getCacheAdapter<TFields extends Record<string, any
|
|
14
|
-
entityConfiguration: EntityConfiguration<TFields>,
|
|
15
|
-
): IEntityCacheAdapter<TFields> {
|
|
13
|
+
getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
|
|
14
|
+
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
15
|
+
): IEntityCacheAdapter<TFields, TIDField> {
|
|
16
16
|
return new GenericEntityCacheAdapter(new GenericRedisCacher(this.context, entityConfiguration));
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Batcher } from '@expo/batcher';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
IEntityGenericCacher,
|
|
4
|
+
SingleFieldHolder,
|
|
5
|
+
SingleFieldValueHolder,
|
|
6
|
+
ViewerContext,
|
|
7
|
+
zipToMap,
|
|
8
|
+
} from '@expo/entity';
|
|
3
9
|
import invariant from 'invariant';
|
|
4
10
|
import Redis from 'ioredis';
|
|
5
11
|
import nullthrows from 'nullthrows';
|
|
@@ -10,9 +16,10 @@ import GenericRedisCacher, {
|
|
|
10
16
|
IRedis,
|
|
11
17
|
IRedisTransaction,
|
|
12
18
|
GenericRedisCacheContext,
|
|
19
|
+
RedisCacheInvalidationStrategy,
|
|
13
20
|
} from '../GenericRedisCacher';
|
|
14
|
-
import RedisTestEntity from '../
|
|
15
|
-
import { createRedisIntegrationTestEntityCompanionProvider } from '../
|
|
21
|
+
import RedisTestEntity, { RedisTestEntityFields } from '../__testfixtures__/RedisTestEntity';
|
|
22
|
+
import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider';
|
|
16
23
|
|
|
17
24
|
class BatchedRedis implements IRedis {
|
|
18
25
|
private readonly mgetBatcher = new Batcher<string[], (string | null)[]>(
|
|
@@ -78,6 +85,7 @@ describe(GenericRedisCacher, () => {
|
|
|
78
85
|
cacheKeyPrefix: 'test-',
|
|
79
86
|
ttlSecondsPositive: 86400, // 1 day
|
|
80
87
|
ttlSecondsNegative: 600, // 10 minutes
|
|
88
|
+
invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
|
|
81
89
|
};
|
|
82
90
|
});
|
|
83
91
|
|
|
@@ -97,14 +105,14 @@ describe(GenericRedisCacher, () => {
|
|
|
97
105
|
|
|
98
106
|
const mgetSpy = jest.spyOn(redis, 'mget');
|
|
99
107
|
|
|
100
|
-
const genericCacher =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
const genericCacher = viewerContext.entityCompanionProvider.getCompanionForEntity(
|
|
109
|
+
RedisTestEntity,
|
|
110
|
+
)['tableDataCoordinator']['cacheAdapter']['genericCacher'] as IEntityGenericCacher<
|
|
111
|
+
RedisTestEntityFields,
|
|
112
|
+
'id'
|
|
113
|
+
>;
|
|
105
114
|
|
|
106
115
|
const entity1Created = await RedisTestEntity.creator(viewerContext)
|
|
107
|
-
.enforcing()
|
|
108
116
|
.setField('name', 'blah')
|
|
109
117
|
.createAsync();
|
|
110
118
|
|
|
@@ -120,11 +128,12 @@ describe(GenericRedisCacher, () => {
|
|
|
120
128
|
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
|
|
121
129
|
);
|
|
122
130
|
const [entity1, entity2, entity3] = await Promise.all([
|
|
123
|
-
RedisTestEntity.loader(viewerContext1).
|
|
124
|
-
RedisTestEntity.loader(viewerContext2).
|
|
125
|
-
RedisTestEntity.loader(viewerContext3)
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
RedisTestEntity.loader(viewerContext1).loadByIDAsync(entity1Created.getID()),
|
|
132
|
+
RedisTestEntity.loader(viewerContext2).loadByIDAsync(entity1Created.getID()),
|
|
133
|
+
RedisTestEntity.loader(viewerContext3).loadByFieldEqualingAsync(
|
|
134
|
+
'name',
|
|
135
|
+
entity1Created.getField('name'),
|
|
136
|
+
),
|
|
128
137
|
]);
|
|
129
138
|
|
|
130
139
|
expect(mgetSpy).toHaveBeenCalledTimes(1);
|
|
@@ -132,7 +141,10 @@ describe(GenericRedisCacher, () => {
|
|
|
132
141
|
expect(entity1.getID()).toEqual(entity2.getID());
|
|
133
142
|
expect(entity2.getID()).toEqual(nullthrows(entity3).getID());
|
|
134
143
|
|
|
135
|
-
const cacheKeyEntity1 =
|
|
144
|
+
const cacheKeyEntity1 = genericCacher.makeCacheKeyForStorage(
|
|
145
|
+
new SingleFieldHolder('id'),
|
|
146
|
+
new SingleFieldValueHolder(entity1Created.getID()),
|
|
147
|
+
);
|
|
136
148
|
const cachedJSON = await redis.get(cacheKeyEntity1);
|
|
137
149
|
const cachedValue = JSON.parse(cachedJSON!);
|
|
138
150
|
expect(cachedValue).toMatchObject({
|
|
@@ -140,31 +152,38 @@ describe(GenericRedisCacher, () => {
|
|
|
140
152
|
name: 'blah',
|
|
141
153
|
});
|
|
142
154
|
|
|
143
|
-
const cacheKeyEntity1NameField =
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
|
|
155
|
+
const cacheKeyEntity1NameField = genericCacher.makeCacheKeyForStorage(
|
|
156
|
+
new SingleFieldHolder('name'),
|
|
157
|
+
new SingleFieldValueHolder(entity1Created.getField('name')),
|
|
158
|
+
);
|
|
159
|
+
await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
|
|
160
|
+
'name',
|
|
161
|
+
entity1Created.getField('name'),
|
|
162
|
+
);
|
|
147
163
|
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toEqual(cachedJSON);
|
|
148
164
|
|
|
149
165
|
// simulate non existent db fetch, should write negative result ('') to cache
|
|
150
166
|
const nonExistentId = uuidv4();
|
|
151
|
-
const entityNonExistentResult =
|
|
152
|
-
.
|
|
153
|
-
|
|
167
|
+
const entityNonExistentResult =
|
|
168
|
+
await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
|
|
169
|
+
nonExistentId,
|
|
170
|
+
);
|
|
154
171
|
expect(entityNonExistentResult.ok).toBe(false);
|
|
155
|
-
const cacheKeyNonExistent =
|
|
172
|
+
const cacheKeyNonExistent = genericCacher.makeCacheKeyForStorage(
|
|
173
|
+
new SingleFieldHolder('id'),
|
|
174
|
+
new SingleFieldValueHolder(nonExistentId),
|
|
175
|
+
);
|
|
156
176
|
const nonExistentCachedValue = await redis.get(cacheKeyNonExistent);
|
|
157
177
|
expect(nonExistentCachedValue).toEqual('');
|
|
158
178
|
// load again through entities framework to ensure it reads negative result
|
|
159
|
-
const entityNonExistentResult2 =
|
|
160
|
-
.
|
|
161
|
-
|
|
179
|
+
const entityNonExistentResult2 =
|
|
180
|
+
await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
|
|
181
|
+
nonExistentId,
|
|
182
|
+
);
|
|
162
183
|
expect(entityNonExistentResult2.ok).toBe(false);
|
|
163
184
|
|
|
164
185
|
// invalidate from cache to ensure it invalidates correctly in both caches
|
|
165
|
-
await RedisTestEntity.
|
|
166
|
-
.utils()
|
|
167
|
-
.invalidateFieldsAsync(entity1.getAllFields());
|
|
186
|
+
await RedisTestEntity.loaderUtils(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
|
|
168
187
|
await expect(redis.get(cacheKeyEntity1)).resolves.toBeNull();
|
|
169
188
|
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toBeNull();
|
|
170
189
|
});
|