diff --git a/packages/entity-cache-adapter-redis/src/GenericRedisCacher.ts b/packages/entity-cache-adapter-redis/src/GenericRedisCacher.ts index 7fc25d4e..e9b9ddec 100644 --- a/packages/entity-cache-adapter-redis/src/GenericRedisCacher.ts +++ b/packages/entity-cache-adapter-redis/src/GenericRedisCacher.ts @@ -6,7 +6,6 @@ import { transformFieldsToCacheObject, IEntityGenericCacher, } from '@expo/entity'; -import { Redis } from 'ioredis'; import { redisTransformerMap } from './RedisCommon'; import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync'; @@ -15,11 +14,22 @@ import wrapNativeRedisCallAsync from './errors/wrapNativeRedisCallAsync'; // The sentinel value is distinct from any (positively) cached value. const DOES_NOT_EXIST_REDIS = ''; +export interface IRedisTransaction { + set(key: string, value: string, secondsToken: 'EX', seconds: number): this; + exec(): Promise; +} + +export interface IRedis { + mget(...args: [...keys: string[]]): Promise<(string | null)[]>; + multi(): IRedisTransaction; + del(...args: [...keys: string[]]): Promise; +} + export interface GenericRedisCacheContext { /** * Instance of ioredis.Redis */ - redisClient: Redis; + redisClient: IRedis; /** * TTL for caching database hits. Successive entity loads within this TTL diff --git a/packages/entity-cache-adapter-redis/src/RedisCacheAdapter.ts b/packages/entity-cache-adapter-redis/src/RedisCacheAdapter.ts index 6cf6b94d..7d1a8cd8 100644 --- a/packages/entity-cache-adapter-redis/src/RedisCacheAdapter.ts +++ b/packages/entity-cache-adapter-redis/src/RedisCacheAdapter.ts @@ -1,14 +1,13 @@ import { EntityCacheAdapter, EntityConfiguration, CacheLoadResult, mapKeys } from '@expo/entity'; import invariant from 'invariant'; -import type { Redis } from 'ioredis'; -import GenericRedisCacher from './GenericRedisCacher'; +import GenericRedisCacher, { IRedis } from './GenericRedisCacher'; export interface RedisCacheAdapterContext { /** * Instance of ioredis.Redis */ - redisClient: Redis; + redisClient: IRedis; /** * Create a key string for key parts (cache key prefix, versions, entity name, etc). diff --git a/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-integration-test.ts b/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-integration-test.ts index 2ae47f98..adbfd5ec 100644 --- a/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-integration-test.ts +++ b/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-integration-test.ts @@ -13,11 +13,12 @@ import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtur class TestViewerContext extends ViewerContext {} describe(GenericRedisCacher, () => { + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(...parts: string[]): string { const delimiter = ':'; const escapedParts = parts.map((part) => @@ -33,10 +34,10 @@ describe(GenericRedisCacher, () => { }); beforeEach(async () => { - await redisCacheAdapterContext.redisClient.flushdb(); + await redisClient.flushdb(); }); afterAll(async () => { - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); }); it('has correct caching and loading behavior', async () => { @@ -62,7 +63,7 @@ describe(GenericRedisCacher, () => { ]); await genericRedisCacher.cacheManyAsync(objectMap); - const cachedJSON = await redisCacheAdapterContext.redisClient.get(testKey); + const cachedJSON = await redisClient.get(testKey); const cachedValue = JSON.parse(cachedJSON!); expect(cachedValue).toMatchObject({ id: entity1Created.getID(), diff --git a/packages/entity-cache-adapter-redis/src/__integration-tests__/RedisCacheAdapter-integration-test.ts b/packages/entity-cache-adapter-redis/src/__integration-tests__/RedisCacheAdapter-integration-test.ts index 3fc1faff..f109662a 100644 --- a/packages/entity-cache-adapter-redis/src/__integration-tests__/RedisCacheAdapter-integration-test.ts +++ b/packages/entity-cache-adapter-redis/src/__integration-tests__/RedisCacheAdapter-integration-test.ts @@ -11,11 +11,12 @@ import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtur class TestViewerContext extends ViewerContext {} describe(RedisCacheAdapter, () => { + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(...parts: string[]): string { const delimiter = ':'; const escapedParts = parts.map((part) => @@ -31,10 +32,10 @@ describe(RedisCacheAdapter, () => { }); beforeEach(async () => { - await redisCacheAdapterContext.redisClient.flushdb(); + await redisClient.flushdb(); }); afterAll(async () => { - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); }); it('has correct caching behavior', async () => { @@ -56,9 +57,7 @@ describe(RedisCacheAdapter, () => { .enforcing() .loadByIDAsync(entity1Created.getID()); - const cachedJSON = await redisCacheAdapterContext.redisClient.get( - cacheKeyMaker('id', entity1.getID()) - ); + const cachedJSON = await redisClient.get(cacheKeyMaker('id', entity1.getID())); const cachedValue = JSON.parse(cachedJSON!); expect(cachedValue).toMatchObject({ id: entity1.getID(), @@ -73,9 +72,7 @@ describe(RedisCacheAdapter, () => { ); expect(entityNonExistentResult.ok).toBe(false); - const nonExistentCachedValue = await redisCacheAdapterContext.redisClient.get( - cacheKeyMaker('id', nonExistentId) - ); + const nonExistentCachedValue = await redisClient.get(cacheKeyMaker('id', nonExistentId)); expect(nonExistentCachedValue).toEqual(''); // load again through entities framework to ensure it reads negative result @@ -86,9 +83,7 @@ describe(RedisCacheAdapter, () => { // invalidate from cache to ensure it invalidates correctly await RedisTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields()); - const cachedValueNull = await redisCacheAdapterContext.redisClient.get( - cacheKeyMaker('id', entity1.getID()) - ); + const cachedValueNull = await redisClient.get(cacheKeyMaker('id', entity1.getID())); expect(cachedValueNull).toBe(null); }); diff --git a/packages/entity-cache-adapter-redis/src/__integration-tests__/errors-test.ts b/packages/entity-cache-adapter-redis/src/__integration-tests__/errors-test.ts index 8c91e533..aa3426d6 100644 --- a/packages/entity-cache-adapter-redis/src/__integration-tests__/errors-test.ts +++ b/packages/entity-cache-adapter-redis/src/__integration-tests__/errors-test.ts @@ -9,11 +9,12 @@ import { createRedisIntegrationTestEntityCompanionProvider } from '../testfixtur class TestViewerContext extends ViewerContext {} describe(RedisCacheAdapter, () => { + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(...parts: string[]): string { const delimiter = ':'; const escapedParts = parts.map((part) => @@ -29,11 +30,11 @@ describe(RedisCacheAdapter, () => { }); beforeEach(async () => { - await redisCacheAdapterContext.redisClient.flushdb(); + await redisClient.flushdb(); }); it('throws when redis is disconnected', async () => { - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); const vc1 = new TestViewerContext( createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext) diff --git a/packages/entity-full-integration-tests/src/__integration-tests__/EntityCacheInconsistency-test.ts b/packages/entity-full-integration-tests/src/__integration-tests__/EntityCacheInconsistency-test.ts index a896e154..b9f25adc 100644 --- a/packages/entity-full-integration-tests/src/__integration-tests__/EntityCacheInconsistency-test.ts +++ b/packages/entity-full-integration-tests/src/__integration-tests__/EntityCacheInconsistency-test.ts @@ -97,6 +97,7 @@ async function dropPostgresTable(knex: Knex): Promise { describe('Entity cache inconsistency', () => { let knexInstance: Knex; + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { @@ -111,7 +112,7 @@ describe('Entity cache inconsistency', () => { }, }); redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(...parts: string[]): string { const delimiter = ':'; const escapedParts = parts.map((part) => @@ -128,13 +129,13 @@ describe('Entity cache inconsistency', () => { beforeEach(async () => { await createOrTruncatePostgresTables(knexInstance); - await redisCacheAdapterContext.redisClient.flushdb(); + await redisClient.flushdb(); }); afterAll(async () => { await dropPostgresTable(knexInstance); await knexInstance.destroy(); - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); }); test('lots of updates in long-ish running transactions', async () => { diff --git a/packages/entity-full-integration-tests/src/__integration-tests__/EntityEdgesIntegration-test.ts b/packages/entity-full-integration-tests/src/__integration-tests__/EntityEdgesIntegration-test.ts index 14e56259..7cc13254 100644 --- a/packages/entity-full-integration-tests/src/__integration-tests__/EntityEdgesIntegration-test.ts +++ b/packages/entity-full-integration-tests/src/__integration-tests__/EntityEdgesIntegration-test.ts @@ -33,6 +33,7 @@ async function dropPostgresTable(knex: Knex): Promise { describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => { let knexInstance: Knex; + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { @@ -47,7 +48,7 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => { }, }); redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(...parts: string[]): string { const delimiter = ':'; const escapedParts = parts.map((part) => @@ -64,13 +65,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => { beforeEach(async () => { await createOrTruncatePostgresTables(knexInstance); - await redisCacheAdapterContext.redisClient.flushdb(); + await redisClient.flushdb(); }); afterAll(async () => { await dropPostgresTable(knexInstance); await knexInstance.destroy(); - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); }); describe('EntityEdgeDeletionBehavior.INVALIDATE_CACHE', () => { diff --git a/packages/entity-full-integration-tests/src/__integration-tests__/EntitySelfReferentialEdgesIntegration-test.ts b/packages/entity-full-integration-tests/src/__integration-tests__/EntitySelfReferentialEdgesIntegration-test.ts index 941ce0ca..b12e6b72 100644 --- a/packages/entity-full-integration-tests/src/__integration-tests__/EntitySelfReferentialEdgesIntegration-test.ts +++ b/packages/entity-full-integration-tests/src/__integration-tests__/EntitySelfReferentialEdgesIntegration-test.ts @@ -176,6 +176,7 @@ const makeEntityClasses = async (knex: Knex, edgeDeletionBehavior: EntityEdgeDel }; describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => { let knexInstance: Knex; + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { @@ -190,7 +191,7 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => { }, }); redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(...parts: string[]): string { const delimiter = ':'; const escapedParts = parts.map((part) => @@ -207,7 +208,7 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => { afterAll(async () => { await knexInstance.destroy(); - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); }); it.each([ diff --git a/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts b/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts index 78cb6ecb..c5785ba0 100644 --- a/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts +++ b/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts @@ -52,11 +52,12 @@ class TestSecondaryRedisCacheLoader extends EntitySecondaryCacheLoader< } describe(RedisSecondaryEntityCache, () => { + const redisClient = new Redis(new URL(process.env['REDIS_URL']!).toString()); let redisCacheAdapterContext: RedisCacheAdapterContext; beforeAll(() => { redisCacheAdapterContext = { - redisClient: new Redis(new URL(process.env['REDIS_URL']!).toString()), + redisClient, makeKeyFn(..._parts: string[]): string { throw new Error('should not be used by this test'); }, @@ -68,10 +69,10 @@ describe(RedisSecondaryEntityCache, () => { }); beforeEach(async () => { - await redisCacheAdapterContext.redisClient.flushdb(); + await redisClient.flushdb(); }); afterAll(async () => { - redisCacheAdapterContext.redisClient.disconnect(); + redisClient.disconnect(); }); it('Loads through secondary loader, caches, and invalidates', async () => {