@expo/entity-cache-adapter-redis 0.41.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.
Files changed (49) hide show
  1. package/build/GenericRedisCacher.d.ts +26 -4
  2. package/build/GenericRedisCacher.js +36 -5
  3. package/build/GenericRedisCacher.js.map +1 -1
  4. package/build/RedisCacheAdapterProvider.d.ts +1 -1
  5. package/build/RedisCacheAdapterProvider.js.map +1 -1
  6. package/build/index.d.ts +1 -0
  7. package/build/index.js +1 -0
  8. package/build/index.js.map +1 -1
  9. package/build/tsconfig.build.tsbuildinfo +1 -0
  10. package/build/utils/getSurroundingCacheKeyVersionsForInvalidation.d.ts +1 -0
  11. package/build/utils/getSurroundingCacheKeyVersionsForInvalidation.js +11 -0
  12. package/build/utils/getSurroundingCacheKeyVersionsForInvalidation.js.map +1 -0
  13. package/package.json +9 -7
  14. package/src/GenericRedisCacher.ts +72 -13
  15. package/src/RedisCacheAdapterProvider.ts +3 -3
  16. package/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts +29 -11
  17. package/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts +83 -18
  18. package/src/__integration-tests__/GenericRedisCacher-integration-test.ts +7 -3
  19. package/src/__integration-tests__/errors-test.ts +7 -3
  20. package/src/{testfixtures → __testfixtures__}/RedisTestEntity.ts +13 -29
  21. package/src/{testfixtures → __testfixtures__}/createRedisIntegrationTestEntityCompanionProvider.ts +1 -2
  22. package/src/__tests__/GenericRedisCacher-test.ts +68 -12
  23. package/src/index.ts +1 -0
  24. package/src/utils/__tests__/getSurroundingCacheKeyVersionsForInvalidation-test.ts +9 -0
  25. package/src/utils/getSurroundingCacheKeyVersionsForInvalidation.ts +9 -0
  26. package/build/__integration-tests__/BatchedRedisCacheAdapter-integration-test.d.ts +0 -1
  27. package/build/__integration-tests__/BatchedRedisCacheAdapter-integration-test.js +0 -122
  28. package/build/__integration-tests__/BatchedRedisCacheAdapter-integration-test.js.map +0 -1
  29. package/build/__integration-tests__/GenericRedisCacher-full-integration-test.d.ts +0 -1
  30. package/build/__integration-tests__/GenericRedisCacher-full-integration-test.js +0 -93
  31. package/build/__integration-tests__/GenericRedisCacher-full-integration-test.js.map +0 -1
  32. package/build/__integration-tests__/GenericRedisCacher-integration-test.d.ts +0 -1
  33. package/build/__integration-tests__/GenericRedisCacher-integration-test.js +0 -122
  34. package/build/__integration-tests__/GenericRedisCacher-integration-test.js.map +0 -1
  35. package/build/__integration-tests__/errors-test.d.ts +0 -1
  36. package/build/__integration-tests__/errors-test.js +0 -39
  37. package/build/__integration-tests__/errors-test.js.map +0 -1
  38. package/build/__tests__/GenericRedisCacher-test.d.ts +0 -1
  39. package/build/__tests__/GenericRedisCacher-test.js +0 -141
  40. package/build/__tests__/GenericRedisCacher-test.js.map +0 -1
  41. package/build/errors/__tests__/wrapNativeRedisCallAsync-test.d.ts +0 -1
  42. package/build/errors/__tests__/wrapNativeRedisCallAsync-test.js +0 -42
  43. package/build/errors/__tests__/wrapNativeRedisCallAsync-test.js.map +0 -1
  44. package/build/testfixtures/RedisTestEntity.d.ts +0 -16
  45. package/build/testfixtures/RedisTestEntity.js +0 -49
  46. package/build/testfixtures/RedisTestEntity.js.map +0 -1
  47. package/build/testfixtures/createRedisIntegrationTestEntityCompanionProvider.d.ts +0 -3
  48. package/build/testfixtures/createRedisIntegrationTestEntityCompanionProvider.js +0 -31
  49. 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>> implements IEntityGenericCacher<TFields> {
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
- makeCacheKey<N extends keyof TFields>(fieldName: N, fieldValue: NonNullable<TFields[N]>): string;
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
- makeCacheKey(fieldName, fieldValue) {
75
- const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
76
- (0, invariant_1.default)(columnName, `database field mapping missing for ${String(fieldName)}`);
77
- return this.context.makeKeyFn(this.context.cacheKeyPrefix, this.entityConfiguration.tableName, `v2.${this.entityConfiguration.cacheKeyVersion}`, columnName, String(fieldValue));
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":";;;;;AAAA,yCAOsB;AACtB,0DAAkC;AAElC,+CAAoD;AACpD,iGAAyE;AAEzE,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,oBAAoB,GAAG,EAAE,CAAC;AA4ChC,MAAqB,kBAAkB;IAIlB;IACA;IAFnB,YACmB,OAAiC,EACjC,mBAAiD;QADjD,YAAO,GAAP,OAAO,CAA0B;QACjC,wBAAmB,GAAnB,mBAAmB,CAA8B;IACjE,CAAC;IAEG,KAAK,CAAC,aAAa,CACxB,IAAuB;QAEvB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,IAAI,GAAG,EAAE,CAAC;QACnB,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAA,kCAAwB,EAAC,GAAG,EAAE,CACvD,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CACvC,CAAC;QAEF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoC,CAAC;QAC5D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;YACrB,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;YAEpC,IAAI,WAAW,KAAK,oBAAoB,EAAE,CAAC;gBACzC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;oBACf,MAAM,EAAE,oBAAW,CAAC,QAAQ;iBAC7B,CAAC,CAAC;YACL,CAAC;iBAAM,IAAI,WAAW,EAAE,CAAC;gBACvB,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;oBACf,MAAM,EAAE,oBAAW,CAAC,GAAG;oBACvB,IAAI,EAAE,IAAA,qCAA4B,EAChC,IAAI,CAAC,mBAAmB,EACxB,iCAAmB,EACnB,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CACxB;iBACF,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;oBACf,MAAM,EAAE,oBAAW,CAAC,IAAI;iBACzB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAEM,KAAK,CAAC,cAAc,CAAC,SAAiD;QAC3E,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QAED,IAAI,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACxD,SAAS,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;YAChC,gBAAgB,GAAG,gBAAgB,CAAC,GAAG,CACrC,GAAG,EACH,IAAI,CAAC,SAAS,CACZ,IAAA,qCAA4B,EAAC,IAAI,CAAC,mBAAmB,EAAE,iCAAmB,EAAE,MAAM,CAAC,CACpF,EACD,IAAI,EACJ,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAChC,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,MAAM,IAAA,kCAAwB,EAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAEM,KAAK,CAAC,kBAAkB,CAAC,IAAuB;QACrD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QAED,IAAI,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;QACxD,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACnB,gBAAgB,GAAG,gBAAgB,CAAC,GAAG,CACrC,GAAG,EACH,oBAAoB,EACpB,IAAI,EACJ,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAChC,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,MAAM,IAAA,kCAAwB,EAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAEM,KAAK,CAAC,mBAAmB,CAAC,IAAuB;QACtD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QAED,MAAM,IAAA,kCAAwB,EAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC9E,CAAC;IAEM,YAAY,CACjB,SAAY,EACZ,UAAmC;QAEnC,MAAM,UAAU,GAAG,IAAI,CAAC,mBAAmB,CAAC,0BAA0B,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACtF,IAAA,mBAAS,EAAC,UAAU,EAAE,sCAAsC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACjF,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAC3B,IAAI,CAAC,OAAO,CAAC,cAAc,EAC3B,IAAI,CAAC,mBAAmB,CAAC,SAAS,EAClC,MAAM,IAAI,CAAC,mBAAmB,CAAC,eAAe,EAAE,EAChD,UAAU,EACV,MAAM,CAAC,UAAU,CAAC,CACnB,CAAC;IACJ,CAAC;CACF;AAxGD,qCAwGC"}
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>>(entityConfiguration: EntityConfiguration<TFields>): IEntityCacheAdapter<TFields>;
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,mBAAiD;QAEjD,OAAO,IAAI,kCAAyB,CAAC,IAAI,4BAAkB,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAClG,CAAC;CACF;AARD,4CAQC"}
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
@@ -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.41.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.41.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
- "@types/jest": "^29.5.12",
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.4.1",
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.2.5",
49
+ "ts-jest": "^29.3.1",
48
50
  "ts-mockito": "^2.6.1",
49
- "typescript": "^5.7.3"
51
+ "typescript": "^5.8.3"
50
52
  },
51
- "gitHead": "ee4be736a9a0ba580e9af0194e07c9cd36b1c599"
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<TFields extends Record<string, any>>
61
- implements IEntityGenericCacher<TFields>
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
- public makeCacheKey<N extends keyof TFields>(
151
- fieldName: N,
152
- fieldValue: NonNullable<TFields[N]>,
153
- ): string {
154
- const columnName = this.entityConfiguration.entityToDBFieldsKeyMapping.get(fieldName);
155
- invariant(columnName, `database field mapping missing for ${String(fieldName)}`);
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.${this.entityConfiguration.cacheKeyVersion}`,
160
- columnName,
161
- String(fieldValue),
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 { ViewerContext, zipToMap } from '@expo/entity';
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 '../testfixtures/RedisTestEntity';
15
- import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
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,11 +105,12 @@ describe(GenericRedisCacher, () => {
97
105
 
98
106
  const mgetSpy = jest.spyOn(redis, 'mget');
99
107
 
100
- const genericCacher =
101
- viewerContext.entityCompanionProvider.getCompanionForEntity(RedisTestEntity)[
102
- 'tableDataCoordinator'
103
- ]['cacheAdapter']['genericCacher'];
104
- const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
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
116
  .setField('name', 'blah')
@@ -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 = cacheKeyMaker('id', entity1Created.getID());
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,7 +152,10 @@ describe(GenericRedisCacher, () => {
140
152
  name: 'blah',
141
153
  });
142
154
 
143
- const cacheKeyEntity1NameField = cacheKeyMaker('name', entity1Created.getField('name'));
155
+ const cacheKeyEntity1NameField = genericCacher.makeCacheKeyForStorage(
156
+ new SingleFieldHolder('name'),
157
+ new SingleFieldValueHolder(entity1Created.getField('name')),
158
+ );
144
159
  await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
145
160
  'name',
146
161
  entity1Created.getField('name'),
@@ -154,7 +169,10 @@ describe(GenericRedisCacher, () => {
154
169
  nonExistentId,
155
170
  );
156
171
  expect(entityNonExistentResult.ok).toBe(false);
157
- const cacheKeyNonExistent = cacheKeyMaker('id', nonExistentId);
172
+ const cacheKeyNonExistent = genericCacher.makeCacheKeyForStorage(
173
+ new SingleFieldHolder('id'),
174
+ new SingleFieldValueHolder(nonExistentId),
175
+ );
158
176
  const nonExistentCachedValue = await redis.get(cacheKeyNonExistent);
159
177
  expect(nonExistentCachedValue).toEqual('');
160
178
  // load again through entities framework to ensure it reads negative result
@@ -1,12 +1,22 @@
1
- import { ViewerContext } from '@expo/entity';
1
+ import {
2
+ CompositeFieldHolder,
3
+ CompositeFieldValueHolder,
4
+ IEntityGenericCacher,
5
+ SingleFieldHolder,
6
+ SingleFieldValueHolder,
7
+ ViewerContext,
8
+ } from '@expo/entity';
2
9
  import { enforceAsyncResult } from '@expo/results';
3
10
  import Redis from 'ioredis';
4
11
  import { URL } from 'url';
5
12
  import { v4 as uuidv4 } from 'uuid';
6
13
 
7
- import GenericRedisCacher, { GenericRedisCacheContext } from '../GenericRedisCacher';
8
- import RedisTestEntity from '../testfixtures/RedisTestEntity';
9
- import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtures/createRedisIntegrationTestEntityCompanionProvider';
14
+ import GenericRedisCacher, {
15
+ GenericRedisCacheContext,
16
+ RedisCacheInvalidationStrategy,
17
+ } from '../GenericRedisCacher';
18
+ import RedisTestEntity, { RedisTestEntityFields } from '../__testfixtures__/RedisTestEntity';
19
+ import { createRedisIntegrationTestEntityCompanionProvider } from '../__testfixtures__/createRedisIntegrationTestEntityCompanionProvider';
10
20
 
11
21
  class TestViewerContext extends ViewerContext {}
12
22
 
@@ -26,6 +36,7 @@ describe(GenericRedisCacher, () => {
26
36
  cacheKeyPrefix: 'test-',
27
37
  ttlSecondsPositive: 86400, // 1 day
28
38
  ttlSecondsNegative: 600, // 10 minutes
39
+ invalidationStrategy: RedisCacheInvalidationStrategy.CURRENT_CACHE_KEY_VERSION,
29
40
  };
30
41
  });
31
42
 
@@ -40,11 +51,12 @@ describe(GenericRedisCacher, () => {
40
51
  const viewerContext = new TestViewerContext(
41
52
  createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext),
42
53
  );
43
- const genericCacher =
44
- viewerContext.entityCompanionProvider.getCompanionForEntity(RedisTestEntity)[
45
- 'tableDataCoordinator'
46
- ]['cacheAdapter']['genericCacher'];
47
- const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
54
+ const genericCacher = viewerContext.entityCompanionProvider.getCompanionForEntity(
55
+ RedisTestEntity,
56
+ )['tableDataCoordinator']['cacheAdapter']['genericCacher'] as IEntityGenericCacher<
57
+ RedisTestEntityFields,
58
+ 'id'
59
+ >;
48
60
 
49
61
  const entity1Created = await RedisTestEntity.creator(viewerContext)
50
62
  .setField('name', 'blah')
@@ -55,15 +67,35 @@ describe(GenericRedisCacher, () => {
55
67
  entity1Created.getID(),
56
68
  );
57
69
 
58
- const cachedJSON = await (genericRedisCacheContext.redisClient as Redis).get(
59
- cacheKeyMaker('id', entity1.getID()),
70
+ const cachedSingleJSON = await (genericRedisCacheContext.redisClient as Redis).get(
71
+ genericCacher.makeCacheKeyForStorage(
72
+ new SingleFieldHolder('id'),
73
+ new SingleFieldValueHolder(entity1.getID()),
74
+ ),
60
75
  );
61
- const cachedValue = JSON.parse(cachedJSON!);
62
- expect(cachedValue).toMatchObject({
76
+ const cachedSingleValue = JSON.parse(cachedSingleJSON!);
77
+ expect(cachedSingleValue).toMatchObject({
63
78
  id: entity1.getID(),
64
79
  name: 'blah',
65
80
  });
66
81
 
82
+ // loading an entity by composite field should put it in cache
83
+ const entity2 = await RedisTestEntity.loader(viewerContext).loadByCompositeFieldEqualingAsync(
84
+ ['name', 'id'],
85
+ { name: 'blah', id: entity1.getID() },
86
+ );
87
+ const cachedCompositeJSON = await (genericRedisCacheContext.redisClient as Redis).get(
88
+ genericCacher.makeCacheKeyForStorage(
89
+ new CompositeFieldHolder<RedisTestEntityFields, 'id'>(['id', 'name']),
90
+ new CompositeFieldValueHolder({ id: entity2!.getID(), name: 'blah' }),
91
+ ),
92
+ );
93
+ const cachedCompositeValue = JSON.parse(cachedCompositeJSON!);
94
+ expect(cachedCompositeValue).toMatchObject({
95
+ id: entity2!.getID(),
96
+ name: 'blah',
97
+ });
98
+
67
99
  // simulate non existent db fetch, should write negative result ('') to cache
68
100
  const nonExistentId = uuidv4();
69
101
 
@@ -72,25 +104,58 @@ describe(GenericRedisCacher, () => {
72
104
  nonExistentId,
73
105
  );
74
106
  expect(entityNonExistentResult.ok).toBe(false);
75
-
76
107
  const nonExistentCachedValue = await (genericRedisCacheContext.redisClient as Redis).get(
77
- cacheKeyMaker('id', nonExistentId),
108
+ genericCacher.makeCacheKeyForStorage(
109
+ new SingleFieldHolder('id'),
110
+ new SingleFieldValueHolder(nonExistentId),
111
+ ),
78
112
  );
79
113
  expect(nonExistentCachedValue).toEqual('');
80
114
 
115
+ const entityNonExistentCompositeResult = await RedisTestEntity.loader(
116
+ viewerContext,
117
+ ).loadByCompositeFieldEqualingAsync(['name', 'id'], { name: 'blah', id: nonExistentId });
118
+ expect(entityNonExistentCompositeResult).toBe(null);
119
+ const nonExistentCompositeCachedValue = await (
120
+ genericRedisCacheContext.redisClient as Redis
121
+ ).get(
122
+ genericCacher.makeCacheKeyForStorage(
123
+ new CompositeFieldHolder<RedisTestEntityFields, 'id'>(['id', 'name']),
124
+ new CompositeFieldValueHolder({ id: nonExistentId, name: 'blah' }),
125
+ ),
126
+ );
127
+ expect(nonExistentCompositeCachedValue).toEqual('');
128
+
81
129
  // load again through entities framework to ensure it reads negative result
82
130
  const entityNonExistentResult2 =
83
131
  await RedisTestEntity.loaderWithAuthorizationResults(viewerContext).loadByIDAsync(
84
132
  nonExistentId,
85
133
  );
86
134
  expect(entityNonExistentResult2.ok).toBe(false);
135
+ const entityNonExistentCompositeResult2 = await RedisTestEntity.loader(
136
+ viewerContext,
137
+ ).loadByCompositeFieldEqualingAsync(['name', 'id'], { name: 'blah', id: nonExistentId });
138
+ expect(entityNonExistentCompositeResult2).toBe(null);
87
139
 
88
140
  // invalidate from cache to ensure it invalidates correctly
89
141
  await RedisTestEntity.loaderUtils(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
90
- const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).get(
91
- cacheKeyMaker('id', entity1.getID()),
142
+ const cachedValueNullKeys = genericCacher.makeCacheKeysForInvalidation(
143
+ new SingleFieldHolder('id'),
144
+ new SingleFieldValueHolder(entity1.getID()),
145
+ );
146
+ const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).mget(
147
+ ...cachedValueNullKeys,
148
+ );
149
+ expect(cachedValueNull.every((c) => c === null)).toBe(true);
150
+
151
+ const cachedValueNullCompositeKeys = genericCacher.makeCacheKeysForInvalidation(
152
+ new CompositeFieldHolder<RedisTestEntityFields, 'id'>(['id', 'name']),
153
+ new CompositeFieldValueHolder({ id: entity1.getID(), name: 'blah' }),
154
+ );
155
+ const cachedValueNullComposite = await (genericRedisCacheContext.redisClient as Redis).mget(
156
+ ...cachedValueNullCompositeKeys,
92
157
  );
93
- expect(cachedValueNull).toBe(null);
158
+ expect(cachedValueNullComposite.every((c) => c === null)).toBe(true);
94
159
  });
95
160
 
96
161
  it('caches and restores date fields', async () => {