Clean up some types
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
+12
-12
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user