diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index aadcee7386..5d56d84c61 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ConcreteCacheClient } from './CacheClient'; +import { DefaultCacheClient } from './CacheClient'; import cacheManager from 'cache-manager'; describe('CacheClient', () => { @@ -33,7 +33,7 @@ describe('CacheClient', () => { describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'somekey'; await sut.get(keyPartial); @@ -42,7 +42,7 @@ describe('CacheClient', () => { }); it('calls client with normalized key (very long key)', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'x'.repeat(251); await sut.get(keyPartial); @@ -55,7 +55,7 @@ describe('CacheClient', () => { it('performs deserialization on returned data', async () => { const expectedValue = { some: 'value' }; - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue)); const actualValue = await sut.get('someKey'); @@ -64,7 +64,7 @@ describe('CacheClient', () => { }); it('returns null on any underlying error', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.get = jest.fn().mockRejectedValue(undefined); const actualValue = await sut.get('someKey'); @@ -75,7 +75,7 @@ describe('CacheClient', () => { describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'somekey'; await sut.set(keyPartial, {}); @@ -86,7 +86,7 @@ describe('CacheClient', () => { }); it('performs serialization on given data', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const expectedData = { some: 'value' }; await sut.set('someKey', expectedData); @@ -97,10 +97,10 @@ describe('CacheClient', () => { }); it('passes ttl to client when given', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const expectedTtl = 3600; - await sut.set('someKey', {}, expectedTtl); + await sut.set('someKey', {}, { ttl: expectedTtl }); const spy = client.set as jest.Mock; const actualOptions = spy.mock.calls[0][2]; @@ -108,7 +108,7 @@ describe('CacheClient', () => { }); it('does not throw errors on any client error', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.set = jest.fn().mockRejectedValue(undefined); expect(async () => { @@ -119,7 +119,7 @@ describe('CacheClient', () => { describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); const keyPartial = 'somekey'; await sut.delete(keyPartial); @@ -130,7 +130,7 @@ describe('CacheClient', () => { }); it('does not throw errors on any client error', async () => { - const sut = new ConcreteCacheClient({ client, pluginId }); + const sut = new DefaultCacheClient({ client, pluginId }); client.del = jest.fn().mockRejectedValue(undefined); expect(async () => { diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 0716b159cb..68c762eb87 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -23,6 +23,10 @@ type CacheClientArgs = { pluginId: string; }; +type CacheSetOptions = { + ttl?: number; +}; + /** * A pre-configured, storage agnostic cache client suitable for use by * Backstage plugins. @@ -38,7 +42,7 @@ export interface CacheClient { * optional TTL may also be provided, otherwise it defaults to the TTL that * was provided when the client was instantiated. */ - set(key: string, value: JsonValue, ttl?: number): Promise; + set(key: string, value: JsonValue, options: CacheSetOptions): Promise; /** * Removes the given key from the cache store. @@ -50,7 +54,7 @@ export interface CacheClient { * A simple, concrete implementation of the CacheClient, suitable for almost * all uses in Backstage. */ -export class ConcreteCacheClient implements CacheClient { +export class DefaultCacheClient implements CacheClient { private readonly client: cacheManager.Cache; private readonly pluginId: string; @@ -63,18 +67,22 @@ export class ConcreteCacheClient implements CacheClient { const k = this.getNormalizedKey(key); try { const data = (await this.client.get(k)) as string | undefined; - return this.unserializeData(data); - } catch (_e) { + return this.deserializeData(data); + } catch { return null; } } - async set(key: string, value: JsonValue, ttl?: number): Promise { + async set( + key: string, + value: JsonValue, + opts: CacheSetOptions = {}, + ): Promise { const k = this.getNormalizedKey(key); try { const data = this.serializeData(value); - await this.client.set(k, data, ttl ? { ttl } : undefined); - } catch (_e) { + await this.client.set(k, data, opts.ttl ? { ttl: opts.ttl } : undefined); + } catch { return; } } @@ -83,7 +91,7 @@ export class ConcreteCacheClient implements CacheClient { const k = this.getNormalizedKey(key); try { await this.client.del(k); - } catch (_e) { + } catch { return; } } @@ -102,14 +110,14 @@ export class ConcreteCacheClient implements CacheClient { return wellFormedKey; } - return createHash('md5').update(candidateKey).digest('hex'); + return createHash('md5').update(candidateKey).digest('base64'); } private serializeData(data: JsonValue): string { return JSON.stringify(data); } - private unserializeData(data: string | undefined): JsonValue { + private deserializeData(data: string | undefined): JsonValue { return data ? JSON.parse(data) : null; } } diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index af8450975a..625c772dec 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -16,13 +16,13 @@ import { ConfigReader } from '@backstage/config'; import cacheManager from 'cache-manager'; -import { ConcreteCacheClient } from './CacheClient'; +import { DefaultCacheClient } from './CacheClient'; import { CacheManager } from './CacheManager'; cacheManager.caching = jest.fn() as jest.Mock; jest.mock('./CacheClient', () => { return { - ConcreteCacheClient: jest.fn(), + DefaultCacheClient: jest.fn(), }; }); @@ -63,9 +63,9 @@ describe('CacheManager', () => { it('connects to a cache store scoped to the plugin', async () => { const pluginId = 'test1'; const expectedTtl = 3600; - manager.forPlugin(pluginId).getClient(expectedTtl); + manager.forPlugin(pluginId).getClient({ defaultTtl: expectedTtl }); - const client = ConcreteCacheClient as jest.Mock; + const client = DefaultCacheClient as jest.Mock; expect(client).toHaveBeenCalledTimes(1); }); @@ -73,11 +73,11 @@ describe('CacheManager', () => { const plugin1Id = 'test1'; const plugin2Id = 'test2'; const expectedTtl = 3600; - manager.forPlugin(plugin1Id).getClient(expectedTtl); - manager.forPlugin(plugin2Id).getClient(expectedTtl); + manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl }); + manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; - const client = ConcreteCacheClient as jest.Mock; + const client = DefaultCacheClient as jest.Mock; expect(cache).toHaveBeenCalledTimes(2); expect(client).toHaveBeenCalledTimes(2); @@ -95,7 +95,7 @@ describe('CacheManager', () => { new ConfigReader({ backend: {} }), ); const expectedTtl = 3600; - manager.forPlugin('test').getClient(expectedTtl); + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); @@ -109,7 +109,7 @@ describe('CacheManager', () => { it('returns memory client when configured', () => { const manager = CacheManager.fromConfig(defaultConfig()); const expectedTtl = 3600; - manager.forPlugin('test').getClient(expectedTtl); + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); @@ -137,7 +137,7 @@ describe('CacheManager', () => { }), ); const expectedTtl = 3600; - manager.forPlugin('test').getClient(expectedTtl); + manager.forPlugin('test').getClient({ defaultTtl: expectedTtl }); const cache = cacheManager.caching as jest.Mock; const mockCalls = cache.mock.calls.splice(-1); diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index de27bb0e3d..065a0b1db5 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -20,7 +20,7 @@ import cacheManager from 'cache-manager'; import Memcache from 'memcache-pp'; // @ts-expect-error import memcachedStore from 'cache-manager-memcached-store'; -import { ConcreteCacheClient, CacheClient } from './CacheClient'; +import { DefaultCacheClient, CacheClient } from './CacheClient'; import { PluginCacheManager } from './types'; /** @@ -30,10 +30,11 @@ import { PluginCacheManager } from './types'; */ export class CacheManager { /** - * Keys represented supported `backend.cache.store` values, mapped to getters - * that return cacheManager.Cache instances appropriate to the store. + * Keys represented supported `backend.cache.store` values, mapped to + * factories that return cacheManager.Cache instances appropriate to the + * store. */ - private readonly storeGetterMap = { + private readonly storeFactories = { memcache: this.getMemcacheClient, memory: this.getMemoryClient, none: this.getNoneClient, @@ -62,9 +63,9 @@ export class CacheManager { */ forPlugin(pluginId: string): PluginCacheManager { return { - getClient: (ttl: number): CacheClient => { - const concreteClient = this.getClientWithTtl(ttl); - return new ConcreteCacheClient({ + getClient: ({ defaultTtl }): CacheClient => { + const concreteClient = this.getClientWithTtl(defaultTtl); + return new DefaultCacheClient({ client: concreteClient, pluginId: pluginId, }); @@ -75,15 +76,15 @@ export class CacheManager { private getClientWithTtl(ttl: number): cacheManager.Cache { const store = this.config.getOptionalString( 'store', - ) as keyof CacheManager['storeGetterMap']; + ) as keyof CacheManager['storeFactories']; - if (this.storeGetterMap.hasOwnProperty(store)) { - return this.storeGetterMap[store].call(this, ttl); + if (this.storeFactories.hasOwnProperty(store)) { + return this.storeFactories[store].call(this, ttl); } - return this.storeGetterMap.none.call(this, ttl); + return this.storeFactories.none.call(this, ttl); } - private getMemcacheClient(ttl: number): cacheManager.Cache { + private getMemcacheClient(defaultTtl: number): cacheManager.Cache { const hosts = this.config.getStringArray('connection.hosts'); const netTimeout = this.config.getOptionalNumber('connection.netTimeout'); return cacheManager.caching({ @@ -93,21 +94,21 @@ export class CacheManager { hosts, ...(netTimeout && { netTimeout }), }, - ttl, + ttl: defaultTtl, }); } - private getMemoryClient(ttl: number): cacheManager.Cache { + private getMemoryClient(defaultTtl: number): cacheManager.Cache { return cacheManager.caching({ store: 'memory', - ttl, + ttl: defaultTtl, }); } - private getNoneClient(ttl: number): cacheManager.Cache { + private getNoneClient(defaultTtl: number): cacheManager.Cache { return cacheManager.caching({ store: 'none', - ttl, + ttl: defaultTtl, }); } } diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index c233621bd4..0cc8178b1a 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -15,5 +15,5 @@ */ export type { CacheClient } from './CacheClient'; -export * from './CacheManager'; -export * from './types'; +export { CacheManager } from './CacheManager'; +export type { PluginCacheManager } from './types'; diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index f1b9e96709..c93455e6b2 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -16,6 +16,10 @@ import { CacheClient } from './CacheClient'; +type ClientOptions = { + defaultTtl: number; +}; + /** * The PluginCacheManager manages access to cache stores that Plugins get. */ @@ -27,5 +31,5 @@ export type PluginCacheManager = { * stores so that plugins are discouraged from cache-level integration * and/or cache key collisions. */ - getClient: (ttl: number) => CacheClient; + getClient: (options: ClientOptions) => CacheClient; };