Skip to content

Commit

Permalink
Support result transformation and caching filtering.
Browse files Browse the repository at this point in the history
  • Loading branch information
corradodellorusso committed Nov 28, 2023
1 parent 8d57a44 commit d381f0e
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 19 deletions.
26 changes: 17 additions & 9 deletions src/memcached.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Store } from 'polycache-core';
import { identity, isCacheable } from './utils';

type Constructor<T> = new (...args: any[]) => T;

export type MemcachedConfig<T = any> = {
driver: Constructor<T>;
options: Record<string, any>;
isCacheable?: (value: unknown) => boolean;
resultTransformer?: (value: unknown) => unknown;
};

export type MemcachedStore<T = any> = Store & {
Expand All @@ -20,20 +23,25 @@ export const createMemcachedStore = <C>(config: MemcachedConfig<C>): MemcachedSt
}

const client = new config.driver(config.options) as any;
const resultTransformerFn = config.resultTransformer ?? identity;
const isCacheableFn = config.isCacheable ?? isCacheable;

return {
name: 'memcached',
client,
get: async (key) => client.get(key),
set: async (key, data, ttl) => client.set(key, data, ttl),
del: async (key) => client.delete(key),
getMany: (...args) => client.getMulti(args).then((obj: Record<string, any>) => Object.keys(obj).map((key) => obj[key])),
setMany: async (args, ttl) => {
Promise.all(args.map((arg) => client.set(...arg, ttl)));
},
delMany: async (...args) => {
Promise.all(args.map((arg) => client.delete(arg)));
get: async <T>(key: string) => client.get(key).then((value: T) => resultTransformerFn(value)),
set: async (key, data, ttl) => {
if (!isCacheableFn(data)) {
return Promise.resolve();
}
return client.set(key, data, ttl);
},
del: async (key) => client.delete(key),
getMany: (...args) =>
client.getMulti(args).then((obj: Record<string, any>) => Object.keys(obj).map((key) => resultTransformerFn(obj[key]))),
setMany: async (args, ttl) =>
Promise.all(args.filter((arg) => isCacheableFn(arg[1])).map((arg) => client.set(...arg, ttl))).then(() => undefined),
delMany: async (...args) => Promise.all(args.map((arg) => client.delete(arg))).then(() => undefined),
reset: () => client.flush(),
ttl: () => {
throw new Error('ttl is not supported on this store.');
Expand Down
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const identity = <T>(input: T): T => input;

export const isCacheable = <T>(input: T): boolean => input !== null && input !== undefined;
69 changes: 59 additions & 10 deletions test/memcached.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@ const config = {
},
};

const createTestMemcachedStore = <T = any>(props: Partial<MemcachedConfig<T>> = {}) => {
return createMemcachedStore({
driver: Memcache,
options: {
hosts: [process.env.MEMCACHED__HOST || config.memcached.host + ':' + config.memcached.port],
testOption: true,
},
...props,
});
};

describe('memcached store', () => {
let cache: Cache<MemcachedStore>;

beforeAll(() => {
cache = caching(
createMemcachedStore({
driver: Memcache,
options: {
hosts: [process.env.MEMCACHED__HOST || config.memcached.host + ':' + config.memcached.port],
testOption: true,
},
}),
);
cache = caching(createTestMemcachedStore());
});

afterAll(async () => cache.reset());
Expand Down Expand Up @@ -60,6 +63,17 @@ describe('memcached store', () => {
const result = await cache.get('foo1');
expect(result).toBe(null);
});

it('should discard non cacheable values', async () => {
const store = createTestMemcachedStore({ isCacheable: (value) => value !== 'bar' });
await store.reset();
await store.set('foo', 'bar');
await store.set('foo1', 'bar1');
const foo = await store.get('foo');
expect(foo).toBe(null);
const foo1 = await store.get('foo1');
expect(foo1).toBe('bar1');
});
});

describe('setMany', () => {
Expand All @@ -73,6 +87,19 @@ describe('memcached store', () => {
const foo1 = await cache.get('foo1');
expect(foo1).toBe('bar1');
});

it('should filter out non cacheable values', async () => {
const store = createTestMemcachedStore({ isCacheable: (value) => value !== 'bar' });
await store.reset();
await store.setMany([
['foo', 'bar'],
['foo1', 'bar1'],
]);
const foo = await store.get('foo');
expect(foo).toBe(null);
const foo1 = await store.get('foo1');
expect(foo1).toBe('bar1');
});
});

describe('get', () => {
Expand All @@ -82,6 +109,17 @@ describe('memcached store', () => {
const result = await cache.get('foo');
expect(result).toBe(value);
});

it('should transform values', async () => {
const store = createTestMemcachedStore({ resultTransformer: (value) => (value === 'bar' ? 'barbar' : value) });
await store.reset();
await store.set('foo', 'bar');
await store.set('foo1', 'bar1');
const foo = await store.get('foo');
expect(foo).toBe('barbar');
const foo1 = await store.get('foo1');
expect(foo1).toBe('bar1');
});
});

describe('getMany', () => {
Expand All @@ -93,6 +131,17 @@ describe('memcached store', () => {
expect(boo).toBe(null);
expect(foo1).toBe('bar1');
});

it('should transform many values', async () => {
const store = createTestMemcachedStore({ resultTransformer: (value) => (value === 'bar' ? 'barbar' : value) });
await store.reset();
await store.set('foo', 'bar');
await store.set('foo1', 'bar1');
const [foo, boo, foo1] = await store.getMany('foo', 'boo', 'foo1');
expect(foo).toBe('barbar');
expect(boo).toBe(null);
expect(foo1).toBe('bar1');
});
});

describe('del', () => {
Expand All @@ -106,7 +155,7 @@ describe('memcached store', () => {
});
});

describe('detMany', () => {
describe('delMany', () => {
it('should delete many values', async () => {
await cache.set('foo', 'bar');
await cache.set('foo1', 'bar1');
Expand Down

0 comments on commit d381f0e

Please sign in to comment.