Clean up some types

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-05-03 16:57:59 +02:00
parent ac8f7e4666
commit b73dc5f46e
6 changed files with 65 additions and 52 deletions
+12 -12
View File
@@ -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 () => {
+18 -10
View File
@@ -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<void>;
set(key: string, value: JsonValue, options: CacheSetOptions): Promise<void>;
/**
* 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<void> {
async set(
key: string,
value: JsonValue,
opts: CacheSetOptions = {},
): Promise<void> {
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;
}
}
+10 -10
View File
@@ -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);
+18 -17
View File
@@ -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,
});
}
}
+2 -2
View File
@@ -15,5 +15,5 @@
*/
export type { CacheClient } from './CacheClient';
export * from './CacheManager';
export * from './types';
export { CacheManager } from './CacheManager';
export type { PluginCacheManager } from './types';
+5 -1
View File
@@ -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;
};