with-simple-caching
Version:
A wrapper that makes it simple to add caching to any function
310 lines • 15.8 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const createExampleCache_1 = require("../../__test_assets__/createExampleCache");
const withSimpleCaching_1 = require("./withSimpleCaching");
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
describe('withSimpleCaching', () => {
describe('synchronous logic, synchronous cache', () => {
it('should only invoke the api once for a wrapped function after the first request', () => {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => {
apiCalls.push(1);
return Math.random();
}, { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn a few times
const result1 = callApi();
const result2 = callApi();
const result3 = callApi();
// check that the response is the same each time
expect(result1).toEqual(result2);
expect(result2).toEqual(result3);
// check that "api" was only called once
expect(apiCalls.length).toEqual(1);
});
it('should invoke the api once for each unique combination of inputs', () => {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(({ name }) => {
apiCalls.push(name);
return Math.random();
}, { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn a few times
const result1 = callApi({ name: 'casey' });
const result2 = callApi({ name: 'katya' });
const result3 = callApi({ name: 'casey' });
const result4 = callApi({ name: 'katya' });
const result5 = callApi({ name: 'casey' });
// check that the response is the same each time the input is the same
expect(result1).toEqual(result3);
expect(result2).toEqual(result4);
expect(result3).toEqual(result5);
// check that "api" was only called twice (once per name)
expect(apiCalls.length).toEqual(2);
});
it('should expose the error, if an error was returned by the function', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const expectedError = new Error('surprise!');
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => {
throw expectedError;
}, { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn and check that we can catch the error
try {
callApi();
throw new Error('should not reach here');
}
catch (error) {
expect(error).toEqual(expectedError);
}
}));
});
describe('asynchronous logic, synchronous cache', () => {
it('should be possible to stringify the result of a promise in the cache', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
yield sleep(100);
return Math.random();
}), { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn a few times
const result1 = yield callApi({ name: 'casey' });
const result2 = yield callApi({ name: 'katya' });
const result3 = yield callApi({ name: 'casey' });
const result4 = yield callApi({ name: 'katya' });
// check that the response is the same each time the input is the same
expect(result1).toEqual(result3);
expect(result2).toEqual(result4);
// check that "api" was only called twice (once per name)
expect(apiCalls.length).toEqual(2);
}));
it('should be possible to wait for the get promise to resolve before deciding whether to set or return', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
return Math.random();
}), { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn a few times
const result1 = yield callApi({ name: 'casey' });
const result2 = yield callApi({ name: 'katya' });
const result3 = yield callApi({ name: 'casey' });
const result4 = yield callApi({ name: 'katya' });
// check that the response is the same each time the input is the same
expect(result1).toEqual(result3);
expect(result2).toEqual(result4);
// check that "api" was only called twice (once per name)
expect(apiCalls.length).toEqual(2);
}));
it('should expose the error, if an error was resolved by the promise returned by the function', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const expectedError = new Error('surprise!');
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => __awaiter(void 0, void 0, void 0, function* () {
throw expectedError;
}), { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn and check that we can catch the error
try {
yield callApi();
throw new Error('should not reach here');
}
catch (error) {
expect(error).toEqual(expectedError);
}
}));
});
describe('(de)serialization', () => {
it('should be possible to use a custom key serialization method', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(({ name }) => {
apiCalls.push(name);
return name;
}, {
cache: (0, createExampleCache_1.createExampleSyncCache)().cache,
serialize: {
key: ({ forInput }) => forInput[0].name.slice(0, 1), // serialize to only the first letter of the name arg
},
});
// call the fn a few times
const result1 = callApi({ name: 'casey' });
const result2 = callApi({ name: 'clarissa' });
const result3 = callApi({ name: 'chloe' });
const result4 = callApi({ name: 'charlotte' });
// check that the response is the same each time
expect(result1).toEqual('casey');
expect(result1).toEqual(result2);
expect(result2).toEqual(result3);
expect(result3).toEqual(result4);
// check that "api" was only called once
expect(apiCalls.length).toEqual(1);
}));
it('should be possible to use a custom value serialization and deserialization method', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const store = {};
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(({ names }) => {
apiCalls.push(names);
return names;
}, {
cache: {
get: (key) => store[key],
set: (key, value) => {
if (typeof value !== 'string')
throw new Error('value was not a string');
store[key] = value;
},
},
serialize: {
value: (returned) => JSON.stringify(returned),
},
deserialize: {
value: (cached) => JSON.parse(cached),
},
});
// call the fn a few times
const result1 = callApi({ names: ['casey'] });
const result2 = callApi({ names: ['chloe', 'charlotte'] });
const result3 = callApi({ names: ['casey'] });
// check that the response is the same each time
expect(result1).toEqual(['casey']);
expect(result1).toEqual(result3);
expect(result2).toEqual(['chloe', 'charlotte']);
// check that "api" was only called once
expect(apiCalls.length).toEqual(2);
}));
it('should ensure cache-miss and cache-hit responses are equivalent in the cases of lossy serde', () => {
// define an example fn
const apiCalls = [];
const store = {};
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(({ names }) => {
apiCalls.push(names);
return names;
}, {
cache: {
get: (key) => store[key],
set: (key, value) => {
if (typeof value !== 'string')
throw new Error('value was not a string');
store[key] = value;
},
},
serialize: {
value: (returned) => JSON.stringify(returned),
},
deserialize: {
value: () => ['balloon'], // it's possible that the users deserialization method looses data. we should make sure their cache-miss response is the same as cache-hit resposes, to fail fast in these situations
},
});
// call the fn a few times
const result1 = callApi({ names: ['casey'] });
const result2 = callApi({ names: ['chloe', 'charlotte'] });
const result3 = callApi({ names: ['casey'] });
const result4 = callApi({ names: ['chloe', 'charlotte'] });
// check that the response is the same each time
expect(result1).toEqual(['balloon']);
expect(result2).toEqual(result1);
expect(result3).toEqual(result2);
expect(result4).toEqual(result3);
// check that "api" was called both times
expect(apiCalls.length).toEqual(2);
});
});
describe('invalidation', () => {
it('should consider the cached value as invalid if value resolved as undefined', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => {
apiCalls.push(1);
return undefined; // return undefined each time
}, {
cache: (0, createExampleCache_1.createExampleSyncCache)().cache,
});
// call the fn a few times
const result1 = callApi();
const result2 = callApi();
const result3 = callApi();
// check that undefined was returned each time
expect(result1).toEqual(undefined);
expect(result2).toEqual(undefined);
expect(result3).toEqual(undefined);
// check that "api" was called each time, since it was not a valid cache value that was "set" each time
expect(apiCalls.length).toEqual(3);
}));
it('should support cache invalidation by calling the cache again if cache value was set to undefined externally', () => __awaiter(void 0, void 0, void 0, function* () {
const { cache, store } = (0, createExampleCache_1.createExampleSyncCache)();
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => {
apiCalls.push(1);
return Math.random();
}, {
cache,
});
// call the fn a few times
const result1 = callApi();
const result2 = callApi();
const result3 = callApi();
// check that the response is the same each time
expect(result1).toEqual(result2);
expect(result2).toEqual(result3);
// check that "api" was only called once
expect(apiCalls.length).toEqual(1);
// now set the value to undefined
expect(store['[]']).toBeDefined(); // sanity check that we've defined the key correctly
store['[]'] = undefined; // invalidate the key written to above
// now call the api again
const result4 = callApi();
// now we expect the value to be different
expect(result4).not.toEqual(result1);
// and we expect the api to have been called again
expect(apiCalls.length).toEqual(2);
}));
it('should treat null as a valid cached value', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => {
apiCalls.push(1);
return null; // return null each time
}, { cache: (0, createExampleCache_1.createExampleSyncCache)().cache });
// call the fn a few times
const result1 = callApi();
const result2 = callApi();
const result3 = callApi();
// check that the response is the same each time
expect(result1).toEqual(result2);
expect(result2).toEqual(result3);
// check that "api" was only called once
expect(apiCalls.length).toEqual(1);
}));
});
describe('expiration', () => {
it('should apply the expiration option correctly', () => __awaiter(void 0, void 0, void 0, function* () {
const { cache, store } = (0, createExampleCache_1.createExampleSyncCache)();
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCaching_1.withSimpleCaching)(() => {
apiCalls.push(1);
return Math.random();
}, {
cache,
expiration: { seconds: 3 }, // wait three seconds until expiration
});
// call the fn
callApi();
// confirm that it passed the secondsUntilExpiration through to the cache
expect(store['[]']).toMatchObject({
options: { expiration: { seconds: 3 } },
});
}));
});
});
//# sourceMappingURL=withSimpleCaching.test.js.map