Swap out cache-manager for Keyv implementation.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Vendored
+5
-13
@@ -75,19 +75,11 @@ export interface Config {
|
||||
}
|
||||
| {
|
||||
store: 'memcache';
|
||||
connection: {
|
||||
/**
|
||||
* An array of memcache hosts, optionally including a port number.
|
||||
* e.g. 127.0.0.1:11211
|
||||
*/
|
||||
hosts: string[];
|
||||
|
||||
/**
|
||||
* Number of milliseconds to wait before assuming there is a
|
||||
* network timeout. Defaults to 500ms.
|
||||
*/
|
||||
netTimeout?: number;
|
||||
};
|
||||
/**
|
||||
* A memcache connection string in the form `user:pass@host:port`.
|
||||
* @secret
|
||||
*/
|
||||
connection: string;
|
||||
};
|
||||
|
||||
cors?: {
|
||||
|
||||
@@ -36,13 +36,10 @@
|
||||
"@backstage/integration": "^0.5.2",
|
||||
"@google-cloud/storage": "^5.8.0",
|
||||
"@octokit/rest": "^18.5.3",
|
||||
"@types/cache-manager": "^3.4.0",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"archiver": "^5.0.2",
|
||||
"cache-manager": "^3.4.3",
|
||||
"cache-manager-memcached-store": "^4.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"concat-stream": "^2.0.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -54,10 +51,11 @@
|
||||
"git-url-parse": "^11.4.4",
|
||||
"helmet": "^4.0.0",
|
||||
"isomorphic-git": "^1.8.0",
|
||||
"keyv": "^4.0.3",
|
||||
"keyv-memcache": "^1.2.5",
|
||||
"knex": "^0.95.1",
|
||||
"lodash": "^4.17.15",
|
||||
"logform": "^2.1.1",
|
||||
"memcache-pp": "^0.3.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimist": "^1.2.5",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -80,7 +78,6 @@
|
||||
"@backstage/cli": "^0.6.10",
|
||||
"@backstage/test-utils": "^0.1.10",
|
||||
"@types/archiver": "^5.1.0",
|
||||
"@types/cache-manager": "^3.4.0",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/concat-stream": "^1.6.0",
|
||||
"@types/fs-extra": "^9.0.3",
|
||||
|
||||
+59
-156
@@ -15,229 +15,132 @@
|
||||
*/
|
||||
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import cacheManager from 'cache-manager';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
describe('CacheClient', () => {
|
||||
const pluginId = 'test';
|
||||
const defaultTtl = 60;
|
||||
const onError = 'returnEmpty';
|
||||
let client: cacheManager.Cache;
|
||||
let client: Keyv;
|
||||
const b64 = (k: string) => Buffer.from(k).toString('base64');
|
||||
|
||||
beforeEach(() => {
|
||||
client = cacheManager.caching({ store: 'none', ttl: 0 });
|
||||
client = new Keyv();
|
||||
client.get = jest.fn();
|
||||
client.set = jest.fn();
|
||||
client.del = jest.fn();
|
||||
client.delete = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('CacheClient.get', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
const keyPartial = 'somekey';
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.get(keyPartial);
|
||||
await sut.get(key);
|
||||
|
||||
expect(client.get).toHaveBeenCalledWith(b64(`${pluginId}:${keyPartial}`));
|
||||
expect(client.get).toHaveBeenCalledWith(b64(key));
|
||||
});
|
||||
|
||||
it('calls client with normalized key (very long key)', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
const keyPartial = 'x'.repeat(251);
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'x'.repeat(251);
|
||||
|
||||
await sut.get(keyPartial);
|
||||
await sut.get(key);
|
||||
|
||||
const spy = client.get as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).not.toEqual(b64(`${pluginId}:${keyPartial}`));
|
||||
expect(actualKey).not.toEqual(b64(key));
|
||||
expect(actualKey.length).toBeLessThan(250);
|
||||
});
|
||||
|
||||
it('performs deserialization on returned data', async () => {
|
||||
const expectedValue = { some: 'value' };
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
client.get = jest.fn().mockResolvedValue(JSON.stringify(expectedValue));
|
||||
|
||||
const actualValue = await sut.get('someKey');
|
||||
|
||||
expect(actualValue).toMatchObject(expectedValue);
|
||||
});
|
||||
|
||||
it('returns undefined on any underlying error', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
client.get = jest.fn().mockRejectedValue(undefined);
|
||||
|
||||
const actualValue = await sut.get('someKey');
|
||||
|
||||
expect(actualValue).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError: 'reject',
|
||||
});
|
||||
it('rejects on underlying error', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.get = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.get('someKey')).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it('resolves what underlying client resolves', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedValue = 'some value';
|
||||
client.get = jest.fn().mockResolvedValue(expectedValue);
|
||||
|
||||
const actualValue = await sut.get('someKey');
|
||||
|
||||
return expect(actualValue).toEqual(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheClient.set', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
const keyPartial = 'somekey';
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.set(keyPartial, {});
|
||||
await sut.set(key, {});
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`));
|
||||
});
|
||||
|
||||
it('performs serialization on given data', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
const expectedData = { some: 'value' };
|
||||
|
||||
await sut.set('someKey', expectedData);
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualData = spy.mock.calls[0][1];
|
||||
expect(actualData).toEqual(JSON.stringify(expectedData));
|
||||
expect(actualKey).toEqual(b64(key));
|
||||
});
|
||||
|
||||
it('passes ttl to client when given', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedTtl = 3600;
|
||||
|
||||
await sut.set('someKey', {}, { ttl: expectedTtl });
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualOptions = spy.mock.calls[0][2];
|
||||
expect(actualOptions.ttl).toEqual(expectedTtl);
|
||||
});
|
||||
|
||||
it('passes defaultTtl to client when not', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
|
||||
await sut.set('someKey', {});
|
||||
|
||||
const spy = client.set as jest.Mock;
|
||||
const actualOptions = spy.mock.calls[0][2];
|
||||
expect(actualOptions.ttl).toEqual(defaultTtl);
|
||||
});
|
||||
|
||||
it('does not reject on any client error by default', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
client.set = jest.fn().mockRejectedValue(undefined);
|
||||
|
||||
return expect(sut.set('someKey', {})).resolves.toBeUndefined();
|
||||
const actualTtl = spy.mock.calls[0][2];
|
||||
expect(actualTtl).toEqual(expectedTtl);
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError: 'reject',
|
||||
});
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.set = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.set('someKey', {})).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it('resolves what underlying client resolves', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedResponse = true;
|
||||
client.set = jest.fn().mockReturnValue(expectedResponse);
|
||||
|
||||
const actualResponse = await sut.set('someKey', {});
|
||||
|
||||
return expect(actualResponse).toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheClient.delete', () => {
|
||||
it('calls client with normalized key', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
const keyPartial = 'somekey';
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const key = 'somekey';
|
||||
|
||||
await sut.delete(keyPartial);
|
||||
await sut.delete(key);
|
||||
|
||||
const spy = client.del as jest.Mock;
|
||||
const spy = client.delete as jest.Mock;
|
||||
const actualKey = spy.mock.calls[0][0];
|
||||
expect(actualKey).toEqual(b64(`${pluginId}:${keyPartial}`));
|
||||
});
|
||||
|
||||
it('does not reject on any client error by default', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError,
|
||||
});
|
||||
client.del = jest.fn().mockRejectedValue(undefined);
|
||||
|
||||
return expect(sut.delete('someKey')).resolves.toBeUndefined();
|
||||
expect(actualKey).toEqual(b64(key));
|
||||
});
|
||||
|
||||
it('rejects on underlying error if configured', async () => {
|
||||
const sut = new DefaultCacheClient({
|
||||
client,
|
||||
defaultTtl,
|
||||
pluginId,
|
||||
onError: 'reject',
|
||||
});
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedError = new Error('Some runtime error');
|
||||
client.del = jest.fn().mockRejectedValue(expectedError);
|
||||
client.delete = jest.fn().mockRejectedValue(expectedError);
|
||||
|
||||
return expect(sut.delete('someKey')).rejects.toEqual(expectedError);
|
||||
});
|
||||
|
||||
it('resolves what underlying client resolves', async () => {
|
||||
const sut = new DefaultCacheClient({ client });
|
||||
const expectedResponse = false;
|
||||
client.delete = jest.fn().mockResolvedValue(expectedResponse);
|
||||
|
||||
const actualResponse = await sut.delete('someKey');
|
||||
|
||||
return expect(actualResponse).toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+21
-57
@@ -15,17 +15,18 @@
|
||||
*/
|
||||
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import cacheManager from 'cache-manager';
|
||||
import { createHash } from 'crypto';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
type CacheClientArgs = {
|
||||
client: cacheManager.Cache;
|
||||
pluginId: string;
|
||||
defaultTtl: number;
|
||||
onError: 'reject' | 'returnEmpty';
|
||||
client: Keyv;
|
||||
};
|
||||
|
||||
type CacheSetOptions = {
|
||||
/**
|
||||
* Optional TTL in milliseconds. Defaults to the TTL provided when the client
|
||||
* was set up (or no TTL if none are provided).
|
||||
*/
|
||||
ttl?: number;
|
||||
};
|
||||
|
||||
@@ -45,80 +46,50 @@ 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, options: CacheSetOptions): Promise<void>;
|
||||
set(key: string, value: JsonValue, options?: CacheSetOptions): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Removes the given key from the cache store.
|
||||
* Removes the given key from the cache store. Resolves true if the key
|
||||
* existed, or false if not.
|
||||
*/
|
||||
delete(key: string): Promise<void>;
|
||||
delete(key: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple, concrete implementation of the CacheClient, suitable for almost
|
||||
* A basic, concrete implementation of the CacheClient, suitable for almost
|
||||
* all uses in Backstage.
|
||||
*/
|
||||
export class DefaultCacheClient implements CacheClient {
|
||||
private readonly client: cacheManager.Cache;
|
||||
private readonly defaultTtl: number;
|
||||
private readonly pluginId: string;
|
||||
private readonly onError: 'reject' | 'returnEmpty';
|
||||
private readonly client: Keyv;
|
||||
|
||||
constructor({ client, defaultTtl, pluginId, onError }: CacheClientArgs) {
|
||||
constructor({ client }: CacheClientArgs) {
|
||||
this.client = client;
|
||||
this.defaultTtl = defaultTtl;
|
||||
this.pluginId = pluginId;
|
||||
this.onError = onError;
|
||||
}
|
||||
|
||||
async get(key: string): Promise<JsonValue | undefined> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
try {
|
||||
const data = (await this.client.get(k)) as string | undefined;
|
||||
return this.deserializeData(data);
|
||||
} catch (e) {
|
||||
if (this.onError === 'reject') {
|
||||
throw e;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return await this.client.get(k);
|
||||
}
|
||||
|
||||
async set(
|
||||
key: string,
|
||||
value: JsonValue,
|
||||
opts: CacheSetOptions = {},
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
try {
|
||||
const data = this.serializeData(value);
|
||||
await this.client.set(k, data, {
|
||||
ttl: opts.ttl || this.defaultTtl,
|
||||
});
|
||||
} catch (e) {
|
||||
if (this.onError === 'reject') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await this.client.set(k, value, opts.ttl)
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
async delete(key: string): Promise<boolean> {
|
||||
const k = this.getNormalizedKey(key);
|
||||
try {
|
||||
await this.client.del(k);
|
||||
} catch (e) {
|
||||
if (this.onError === 'reject') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return await this.client.delete(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespaces key by plugin to discourage cross-plugin integration via the
|
||||
* cache store.
|
||||
* Ensures keys are well-formed for any/all cache stores.
|
||||
*/
|
||||
private getNormalizedKey(key: string): string {
|
||||
// Namespace key by plugin ID and remove potentially invalid characters.
|
||||
const candidateKey = `${this.pluginId}:${key}`;
|
||||
private getNormalizedKey(candidateKey: string): string {
|
||||
// Remove potentially invalid characters.
|
||||
const wellFormedKey = Buffer.from(candidateKey).toString('base64');
|
||||
|
||||
// Memcache in particular doesn't do well with keys > 250 bytes.
|
||||
@@ -129,11 +100,4 @@ export class DefaultCacheClient implements CacheClient {
|
||||
return createHash('md5').update(candidateKey).digest('base64');
|
||||
}
|
||||
|
||||
private serializeData(data: JsonValue): string {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
private deserializeData(data: string | undefined): JsonValue | undefined {
|
||||
return data ? JSON.parse(data) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-35
@@ -15,11 +15,16 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import cacheManager from 'cache-manager';
|
||||
import Keyv from 'keyv';
|
||||
/* @ts-expect-error */
|
||||
import KeyvMemcache from 'keyv-memcache';
|
||||
import { DefaultCacheClient } from './CacheClient';
|
||||
import { CacheManager } from './CacheManager';
|
||||
|
||||
cacheManager.caching = jest.fn() as jest.Mock;
|
||||
jest.createMockFromModule('keyv');
|
||||
jest.mock('keyv');
|
||||
jest.createMockFromModule('keyv-memcache');
|
||||
jest.mock('keyv-memcache');
|
||||
jest.mock('./CacheClient', () => {
|
||||
return {
|
||||
DefaultCacheClient: jest.fn(),
|
||||
@@ -40,18 +45,14 @@ describe('CacheManager', () => {
|
||||
|
||||
describe('CacheManager.fromConfig', () => {
|
||||
it('accesses the backend.cache key', () => {
|
||||
const getOptionalConfig = jest.fn();
|
||||
const getOptionalString = jest.fn();
|
||||
const config = defaultConfig();
|
||||
config.getOptionalConfig = getOptionalConfig;
|
||||
config.getOptionalString = getOptionalString;
|
||||
|
||||
CacheManager.fromConfig(config);
|
||||
|
||||
expect(getOptionalString.mock.calls[0][0]).toEqual('backend.cache.store');
|
||||
expect(getOptionalConfig.mock.calls[0][0]).toEqual(
|
||||
'backend.cache.connection',
|
||||
);
|
||||
expect(getOptionalString.mock.calls[1][0]).toEqual('backend.cache.connection');
|
||||
});
|
||||
|
||||
it('does not require the backend.cache key', () => {
|
||||
@@ -76,8 +77,7 @@ describe('CacheManager', () => {
|
||||
|
||||
it('connects to a cache store scoped to the plugin', async () => {
|
||||
const pluginId = 'test1';
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin(pluginId).getClient({ defaultTtl: expectedTtl });
|
||||
manager.forPlugin(pluginId).getClient();
|
||||
|
||||
const client = DefaultCacheClient as jest.Mock;
|
||||
expect(client).toHaveBeenCalledTimes(1);
|
||||
@@ -90,62 +90,60 @@ describe('CacheManager', () => {
|
||||
manager.forPlugin(plugin1Id).getClient({ defaultTtl: expectedTtl });
|
||||
manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = cacheManager.caching as jest.Mock;
|
||||
const client = DefaultCacheClient as jest.Mock;
|
||||
const cache = Keyv as unknown as jest.Mock;
|
||||
expect(cache).toHaveBeenCalledTimes(2);
|
||||
expect(client).toHaveBeenCalledTimes(2);
|
||||
|
||||
const plugin1CallArgs = client.mock.calls[0];
|
||||
const plugin2CallArgs = client.mock.calls[1];
|
||||
expect(plugin1CallArgs[0].pluginId).not.toEqual(
|
||||
plugin2CallArgs[0].pluginId,
|
||||
const plugin1CallArgs = cache.mock.calls[0];
|
||||
const plugin2CallArgs = cache.mock.calls[1];
|
||||
expect(plugin1CallArgs[0].namespace).not.toEqual(
|
||||
plugin2CallArgs[0].namespace,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CacheManager.forPlugin stores', () => {
|
||||
it('returns none client when no cache is configured', () => {
|
||||
it('returns memory client when no cache is configured', () => {
|
||||
const manager = CacheManager.fromConfig(
|
||||
new ConfigReader({ backend: {} }),
|
||||
);
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
|
||||
const expectedNamespace = 'test-plugin';
|
||||
manager.forPlugin(expectedNamespace).getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = cacheManager.caching as jest.Mock;
|
||||
const cache = Keyv as unknown as jest.Mock;
|
||||
const mockCalls = cache.mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0]).toMatchObject({
|
||||
store: 'none',
|
||||
ttl: expectedTtl,
|
||||
namespace: expectedNamespace,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns memory client when configured', () => {
|
||||
it('returns memory client when explicitly configured', () => {
|
||||
const manager = CacheManager.fromConfig(defaultConfig());
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
|
||||
const expectedNamespace = 'test-plugin';
|
||||
manager.forPlugin(expectedNamespace).getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = cacheManager.caching as jest.Mock;
|
||||
const cache = Keyv as unknown as jest.Mock;
|
||||
const mockCalls = cache.mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0]).toMatchObject({
|
||||
store: 'memory',
|
||||
ttl: expectedTtl,
|
||||
namespace: expectedNamespace,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a memcache client when configured', () => {
|
||||
const expectedHost = '127.0.0.1:11211';
|
||||
const expectedTimeout = 1000;
|
||||
const manager = CacheManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
cache: {
|
||||
store: 'memcache',
|
||||
connection: {
|
||||
hosts: [expectedHost],
|
||||
netTimeout: expectedTimeout,
|
||||
},
|
||||
connection: expectedHost,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -153,16 +151,14 @@ describe('CacheManager', () => {
|
||||
const expectedTtl = 3600;
|
||||
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
|
||||
|
||||
const cache = cacheManager.caching as jest.Mock;
|
||||
const mockCalls = cache.mock.calls.splice(-1);
|
||||
const callArgs = mockCalls[0];
|
||||
expect(callArgs[0]).toMatchObject({
|
||||
options: {
|
||||
hosts: [expectedHost],
|
||||
netTimeout: expectedTimeout,
|
||||
},
|
||||
const cache = Keyv as unknown as jest.Mock;
|
||||
const mockCacheCalls = cache.mock.calls.splice(-1);
|
||||
expect(mockCacheCalls[0][0]).toMatchObject({
|
||||
ttl: expectedTtl,
|
||||
});
|
||||
const memcache = KeyvMemcache as jest.Mock;
|
||||
const mockMemcacheCalls = memcache.mock.calls.splice(-1);
|
||||
expect(mockMemcacheCalls[0][0]).toEqual(expectedHost);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+29
-50
@@ -14,34 +14,30 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import cacheManager from 'cache-manager';
|
||||
import { Config } from '@backstage/config';
|
||||
import Keyv from 'keyv';
|
||||
// @ts-expect-error
|
||||
import Memcache from 'memcache-pp';
|
||||
// @ts-expect-error
|
||||
import memcachedStore from 'cache-manager-memcached-store';
|
||||
import KeyvMemcache from 'keyv-memcache';
|
||||
import { DefaultCacheClient, CacheClient } from './CacheClient';
|
||||
import { PluginCacheManager } from './types';
|
||||
|
||||
/**
|
||||
* Implements a Cache Manager which will automatically create new cache clients
|
||||
* for plugins when requested. All requested cacheclients are created with the
|
||||
* credentials provided.
|
||||
* connection details provided.
|
||||
*/
|
||||
export class CacheManager {
|
||||
/**
|
||||
* Keys represents supported `backend.cache.store` values, mapped to
|
||||
* factories that return cacheManager.Cache instances appropriate to the
|
||||
* store.
|
||||
* Keys represent supported `backend.cache.store` values, mapped to factories
|
||||
* that return Keyv instances appropriate to the store.
|
||||
*/
|
||||
private readonly storeFactories = {
|
||||
memcache: this.getMemcacheClient,
|
||||
memory: this.getMemoryClient,
|
||||
none: this.getNoneClient,
|
||||
};
|
||||
|
||||
private readonly store: keyof CacheManager['storeFactories'];
|
||||
private readonly connection: Config;
|
||||
private readonly connection: string;
|
||||
|
||||
/**
|
||||
* Creates a new CacheManager instance by reading from the `backend` config
|
||||
@@ -51,21 +47,18 @@ export class CacheManager {
|
||||
*/
|
||||
static fromConfig(config: Config): CacheManager {
|
||||
// If no `backend.cache` config is provided, instantiate the CacheManager
|
||||
// with empty config; allowing a "none" cache client will be returned.
|
||||
const store = config.getOptionalString('backend.cache.store') || 'none';
|
||||
const connectionConfig =
|
||||
config.getOptionalConfig('backend.cache.connection') ||
|
||||
new ConfigReader(undefined);
|
||||
|
||||
return new CacheManager(store, connectionConfig);
|
||||
// with a "memory" cache client.
|
||||
const store = config.getOptionalString('backend.cache.store') || 'memory';
|
||||
const connectionString = config.getOptionalString('backend.cache.connection') || '';
|
||||
return new CacheManager(store, connectionString);
|
||||
}
|
||||
|
||||
private constructor(store: string, connectionConfig: Config) {
|
||||
private constructor(store: string, connectionString: string) {
|
||||
if (!this.storeFactories.hasOwnProperty(store)) {
|
||||
throw new Error(`Unknown cache store: ${store}`);
|
||||
}
|
||||
this.store = store as keyof CacheManager['storeFactories'];
|
||||
this.connection = connectionConfig;
|
||||
this.connection = connectionString;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,47 +68,33 @@ export class CacheManager {
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginCacheManager {
|
||||
return {
|
||||
getClient: ({ defaultTtl, onError }): CacheClient => {
|
||||
const concreteClient = this.getClientWithTtl(defaultTtl);
|
||||
getClient: (opts = {}): CacheClient => {
|
||||
const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl);
|
||||
return new DefaultCacheClient({
|
||||
client: concreteClient,
|
||||
defaultTtl,
|
||||
pluginId: pluginId,
|
||||
onError: onError || 'returnEmpty',
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getClientWithTtl(ttl: number): cacheManager.Cache {
|
||||
return this.storeFactories[this.store].call(this, ttl);
|
||||
private getClientWithTtl(pluginId: string, ttl: number | undefined): Keyv {
|
||||
return this.storeFactories[this.store].call(this, pluginId, ttl);
|
||||
}
|
||||
|
||||
private getMemcacheClient(defaultTtl: number): cacheManager.Cache {
|
||||
const hosts = this.connection.getStringArray('hosts');
|
||||
const netTimeout = this.connection.getOptionalNumber('netTimeout');
|
||||
return cacheManager.caching({
|
||||
store: memcachedStore,
|
||||
driver: Memcache,
|
||||
options: {
|
||||
hosts,
|
||||
...(netTimeout && { netTimeout }),
|
||||
},
|
||||
private getMemcacheClient(pluginId: string, defaultTtl: number | undefined): Keyv {
|
||||
const memcache = new KeyvMemcache(this.connection);
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
ttl: defaultTtl,
|
||||
store: memcache,
|
||||
});
|
||||
}
|
||||
|
||||
private getMemoryClient(pluginId: string, defaultTtl: number | undefined): Keyv {
|
||||
return new Keyv({
|
||||
namespace: pluginId,
|
||||
ttl: defaultTtl,
|
||||
});
|
||||
}
|
||||
|
||||
private getMemoryClient(defaultTtl: number): cacheManager.Cache {
|
||||
return cacheManager.caching({
|
||||
store: 'memory',
|
||||
ttl: defaultTtl,
|
||||
});
|
||||
}
|
||||
|
||||
private getNoneClient(defaultTtl: number): cacheManager.Cache {
|
||||
return cacheManager.caching({
|
||||
store: 'none',
|
||||
ttl: defaultTtl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -17,8 +17,12 @@
|
||||
import { CacheClient } from './CacheClient';
|
||||
|
||||
type ClientOptions = {
|
||||
defaultTtl: number;
|
||||
onError?: 'reject' | 'returnEmpty';
|
||||
/**
|
||||
* An optional default TTL (in milliseconds) to be set when getting a client
|
||||
* instance. If not provided, data will persist indefinitely by default (or
|
||||
* can be configured per entry at set-time).
|
||||
*/
|
||||
defaultTtl?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -32,5 +36,5 @@ export type PluginCacheManager = {
|
||||
* stores so that plugins are discouraged from cache-level integration
|
||||
* and/or cache key collisions.
|
||||
*/
|
||||
getClient: (options: ClientOptions) => CacheClient;
|
||||
getClient: (options?: ClientOptions) => CacheClient;
|
||||
};
|
||||
|
||||
@@ -5737,11 +5737,6 @@
|
||||
resolved "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.0.tgz#e190a5a548e0b348adb0df9ac7fa5f1151c7cca4"
|
||||
integrity sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==
|
||||
|
||||
"@types/cache-manager@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.0.tgz#414136ea3807a8cd071b8f20370c5df5dbffd382"
|
||||
integrity sha512-XVbn2HS+O+Mk2SKRCjr01/8oD5p2Tv1fxxdBqJ0+Cl+UBNiz0WVY5rusHpMGx+qF6Vc2pnRwPVwSKbGaDApCpw==
|
||||
|
||||
"@types/cacheable-request@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"
|
||||
@@ -8157,11 +8152,6 @@ async@0.9.x:
|
||||
resolved "https://registry.npmjs.org/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
|
||||
integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
|
||||
|
||||
async@3.2.0, async@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
async@^2.0.1, async@^2.6.1, async@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
@@ -8836,7 +8826,7 @@ block-stream@*:
|
||||
dependencies:
|
||||
inherits "~2.0.0"
|
||||
|
||||
bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.4.1, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2:
|
||||
bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
@@ -9261,20 +9251,6 @@ cache-base@^1.0.1:
|
||||
union-value "^1.0.0"
|
||||
unset-value "^1.0.0"
|
||||
|
||||
cache-manager-memcached-store@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/cache-manager-memcached-store/-/cache-manager-memcached-store-4.0.0.tgz#f68d00cdfaa73696d13137b7b0f39397167d9220"
|
||||
integrity sha512-Lv1WMaRC1FQHHqxE8Xbi7YWInhgfxjOmLEE6u1K0A3Rwmq+XRLyekKDM7aW9WIzF+PuII/RDKqBK/Ezo5H2QVw==
|
||||
|
||||
cache-manager@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.3.tgz#c978d58f82b414ade08903d4d72b56a80a4978be"
|
||||
integrity sha512-6+Hfzy1SNs/thUwo+07pV0ozgxc4sadrAN0eFVGvXl/X9nz3J0BqEnnEoyxEn8jnF+UkEo0MKpyk9BO80hMeiQ==
|
||||
dependencies:
|
||||
async "3.2.0"
|
||||
lodash "^4.17.21"
|
||||
lru-cache "6.0.0"
|
||||
|
||||
cacheable-lookup@^5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3"
|
||||
@@ -9481,11 +9457,6 @@ capture-exit@^2.0.0:
|
||||
dependencies:
|
||||
rsvp "^4.8.4"
|
||||
|
||||
carrier@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/carrier/-/carrier-0.3.0.tgz#bd295d1d3a7524cac63dd779b929ee22a362bad4"
|
||||
integrity sha1-vSldHTp1JMrGPdd5uSnuIqNiutQ=
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7"
|
||||
@@ -10302,11 +10273,6 @@ connect-history-api-fallback@^1.6.0:
|
||||
resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
|
||||
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
|
||||
|
||||
connection-parse@0.0.x:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz#18e7318aab06a699267372b10c5226d25a1c9a69"
|
||||
integrity sha1-GOcxiqsGppkmc3KxDFIm0locmmk=
|
||||
|
||||
console-browserify@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||
@@ -14489,9 +14455,9 @@ graphql-extensions@^0.12.8:
|
||||
apollo-server-env "^3.0.0"
|
||||
apollo-server-types "^0.6.3"
|
||||
|
||||
graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.8.2:
|
||||
graphql-language-service-interface@^2.8.2:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b"
|
||||
resolved "https://registry.yarnpkg.com/graphql-language-service-interface/-/graphql-language-service-interface-2.8.2.tgz#b3bb2aef7eaf0dff0b4ea419fa412c5f66fa268b"
|
||||
integrity sha512-otbOQmhgkAJU1QJgQkMztNku6SbJLu/uodoFOYOOtJsizTjrMs93vkYaHCcYnLA3oi1Goj27XcHjMnRCYQOZXQ==
|
||||
dependencies:
|
||||
graphql-language-service-parser "^1.9.0"
|
||||
@@ -14499,9 +14465,9 @@ graphql-language-service-interface@2.8.2, graphql-language-service-interface@^2.
|
||||
graphql-language-service-utils "^2.5.1"
|
||||
vscode-languageserver-types "^3.15.1"
|
||||
|
||||
graphql-language-service-parser@1.9.0, graphql-language-service-parser@^1.9.0:
|
||||
graphql-language-service-parser@^1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724"
|
||||
resolved "https://registry.yarnpkg.com/graphql-language-service-parser/-/graphql-language-service-parser-1.9.0.tgz#79af21294119a0a7e81b6b994a1af36833bab724"
|
||||
integrity sha512-B5xPZLbBmIp0kHvpY1Z35I5DtPoDK9wGxQVRDIzcBaiIvAmlTrDvjo3bu7vKREdjFbYKvWNgrEWENuprMbF17Q==
|
||||
dependencies:
|
||||
graphql-language-service-types "^1.8.0"
|
||||
@@ -14780,14 +14746,6 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
||||
inherits "^2.0.3"
|
||||
minimalistic-assert "^1.0.1"
|
||||
|
||||
hashring@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz#fda4efde8aa22cdb97fb1d2a65e88401e1c144ce"
|
||||
integrity sha1-/aTv3oqiLNuX+x0qZeiEAeHBRM4=
|
||||
dependencies:
|
||||
connection-parse "0.0.x"
|
||||
simple-lru-cache "0.0.x"
|
||||
|
||||
hast-util-parse-selector@^2.0.0:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.4.tgz#60c99d0b519e12ab4ed32e58f150ec3f61ed1974"
|
||||
@@ -16939,7 +16897,7 @@ json-buffer@3.0.0:
|
||||
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
||||
|
||||
json-buffer@3.0.1:
|
||||
json-buffer@3.0.1, json-buffer@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
|
||||
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
|
||||
@@ -17291,6 +17249,14 @@ keytar@^5.4.0:
|
||||
nan "2.14.1"
|
||||
prebuild-install "5.3.3"
|
||||
|
||||
keyv-memcache@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/keyv-memcache/-/keyv-memcache-1.2.5.tgz#9097af5c617dc740e7300ebfd4a2efb8917429e3"
|
||||
integrity sha512-iG+GHlhXyV83gmlCtMFIqNYVvv0i0towAy42NvziUR7D+k9jp81fAq0lu66H4gooOnW+ojkdigRvRpL40j1f7w==
|
||||
dependencies:
|
||||
json-buffer "^3.0.1"
|
||||
memjs "^1.3.0"
|
||||
|
||||
keyv@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
|
||||
@@ -17305,6 +17271,13 @@ keyv@^4.0.0:
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
keyv@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.3.tgz#4f3aa98de254803cafcd2896734108daa35e4254"
|
||||
integrity sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==
|
||||
dependencies:
|
||||
json-buffer "3.0.1"
|
||||
|
||||
killable@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
||||
@@ -18065,13 +18038,6 @@ lru-cache@2:
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
|
||||
integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=
|
||||
|
||||
lru-cache@6.0.0, lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^4.0.1:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
||||
@@ -18087,6 +18053,13 @@ lru-cache@^5.0.0, lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
|
||||
@@ -18378,16 +18351,10 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
memcache-pp@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.npmjs.org/memcache-pp/-/memcache-pp-0.3.3.tgz#3124830e27d55a252499726510841590ac130c99"
|
||||
integrity sha512-fMitetT5ulUs4DnFaoVqWHrTBiddLG/l8rMHXbV+ibuL7uWjBulaEf8koTBEweyrjmflqhbH4Uy9V709LadXSA==
|
||||
dependencies:
|
||||
bluebird "^3.4.1"
|
||||
carrier "^0.3.0"
|
||||
debug "^2.6.9"
|
||||
hashring "^3.2.0"
|
||||
immutable "^3.8.1"
|
||||
memjs@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/memjs/-/memjs-1.3.0.tgz#b7959b4ff3770e4c785463fd147f1e4fafd47a24"
|
||||
integrity sha512-y/V9a0auepA9Lgyr4QieK6K2FczjHucEdTpSS+hHVNmVEkYxruXhkHu8n6DSRQ4HXHEE3cc6Sf9f88WCJXGXsQ==
|
||||
|
||||
memoize-one@^5.1.1:
|
||||
version "5.1.1"
|
||||
@@ -23833,11 +23800,6 @@ simple-get@^3.0.2, simple-get@^3.0.3:
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
simple-lru-cache@0.0.x:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz#d59cc3a193c1a5d0320f84ee732f6e4713e511dd"
|
||||
integrity sha1-1ZzDoZPBpdAyD4Tucy9uRxPlEd0=
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
@@ -25779,11 +25741,16 @@ typescript-json-schema@^0.49.0:
|
||||
typescript "^4.1.3"
|
||||
yargs "^16.2.0"
|
||||
|
||||
typescript@^4.0.3, typescript@^4.1.3, typescript@~4.1.3:
|
||||
typescript@^4.0.3, typescript@^4.1.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
|
||||
integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
|
||||
|
||||
typescript@~4.1.3:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
|
||||
integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.28"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||
|
||||
Reference in New Issue
Block a user