with-simple-caching
Version:
A wrapper that makes it simple to add caching to any function
427 lines • 24.6 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 simple_in_memory_cache_1 = require("simple-in-memory-cache");
const createExampleCache_1 = require("../../__test_assets__/createExampleCache");
const withSimpleCachingAsync_1 = require("./withSimpleCachingAsync");
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
describe('withSimpleCachingAsync', () => {
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, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ 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, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ 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, withSimpleCachingAsync_1.withSimpleCachingAsync)(() => __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('asynchronous logic, asynchronous cache', () => {
it('should be possible for a cache to await and persist the resolved value, not the promise', () => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const { cache, store } = (0, createExampleCache_1.createExampleAsyncCache)();
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
yield sleep(100);
return Math.random();
}), { 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);
// check that the value in the cache is not the promise, but the value itself
expect(typeof Promise.resolve(821)).toEqual('object'); // prove that a promise to resolve a number has a typeof object
expect(typeof ((_a = store[JSON.stringify([{ name: 'casey' }])]) === null || _a === void 0 ? void 0 : _a.value)).toEqual('number'); // now prove that the value saved into the cache for this name is definetly not a promise
}));
it('should deduplicate parallel requests in memory even before async cache has finished resolving its first get', () => __awaiter(void 0, void 0, void 0, function* () {
const store = {};
const cache = {
set: (key, value) => __awaiter(void 0, void 0, void 0, function* () {
store[key] = value;
}),
get: (key) => __awaiter(void 0, void 0, void 0, function* () {
yield sleep(1500); // act like it takes a while for the cache to resolve
return store[key];
}),
};
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
yield sleep(100);
return Math.random();
}), { cache });
// call the fn a few times, in parallel
const [result1, result2, result3, result4] = yield Promise.all([
callApi({ name: 'casey' }),
callApi({ name: 'katya' }),
callApi({ name: 'casey' }),
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);
// check that the value in the cache is not the promise, but the value itself
expect(typeof Promise.resolve(821)).toEqual('object'); // prove that a promise to resolve a number has a typeof object
expect(typeof store[JSON.stringify([{ name: 'casey' }])]).toEqual('number'); // now prove that the value saved into the cache for this name is definetly not a promise
}));
it('should deduplicate parallel requests in memory via the passed in in-memory cache if one was passed in', () => __awaiter(void 0, void 0, void 0, function* () {
const store = {};
const cache = {
set: (key, value) => __awaiter(void 0, void 0, void 0, function* () {
store[key] = value;
}),
get: (key) => __awaiter(void 0, void 0, void 0, function* () {
yield sleep(1500); // act like it takes a while for the cache to resolve
return store[key];
}),
};
// define an example fn
const apiCalls = [];
const deduplicationCache = (0, simple_in_memory_cache_1.createCache)();
const callApi = (args) =>
// note that this function instantiates a new wrapper each time -> requiring the deduplication cache to be passed in
(0, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
yield sleep(100);
return Math.random();
}), { cache: { output: cache, deduplication: deduplicationCache } })(args);
// call the fn a few times, in parallel
const [result1, result2, result3, result4] = yield Promise.all([
callApi({ name: 'casey' }),
callApi({ name: 'katya' }),
callApi({ name: 'casey' }),
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);
// check that the value in the cache is not the promise, but the value itself
expect(typeof Promise.resolve(821)).toEqual('object'); // prove that a promise to resolve a number has a typeof object
expect(typeof store[JSON.stringify([{ name: 'casey' }])]).toEqual('number'); // now prove that the value saved into the cache for this name is definetly not a promise
}));
it('should be possible to catch an error which was rejected by a promise set to the cache in an async cache which awaited the value onSet', () => __awaiter(void 0, void 0, void 0, function* () {
var _b;
const { cache, store } = (0, createExampleCache_1.createExampleAsyncCache)();
// define an example fn
const expectedError = new Error('surprise!');
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(
// eslint-disable-next-line no-empty-pattern
({}) => __awaiter(void 0, void 0, void 0, function* () {
throw expectedError;
}), { cache });
// prove that we can catch the error
try {
yield callApi({ name: 'casey' });
throw new Error('should not reach here');
}
catch (error) {
expect(error).toEqual(expectedError);
}
// prove that nothing was set to the cache
expect(typeof ((_b = store[JSON.stringify([{ name: 'casey' }])]) === null || _b === void 0 ? void 0 : _b.value)).toEqual('undefined');
}));
it('should have appropriate types for an async cache that caches awaited values', () => __awaiter(void 0, void 0, void 0, function* () {
var _c;
const { cache, store } = (0, createExampleCache_1.createExampleAsyncCache)();
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
yield sleep(100);
return Math.random();
}), { 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);
// check that the value in the cache is not the promise, but the value itself
expect(typeof Promise.resolve(821)).toEqual('object'); // prove that a promise to resolve a number has a typeof object
expect(typeof ((_c = store[JSON.stringify([{ name: 'casey' }])]) === null || _c === void 0 ? void 0 : _c.value)).toEqual('number'); // now prove that the value saved into the cache for this name is definetly not a promise
}));
});
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, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
return name;
}), {
cache: (0, createExampleCache_1.createExampleAsyncCache)().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 = yield callApi({ name: 'casey' });
const result2 = yield callApi({ name: 'clarissa' });
const result3 = yield callApi({ name: 'chloe' });
const result4 = yield 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, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ names }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(names);
return names;
}), {
cache: {
get: (key) => __awaiter(void 0, void 0, void 0, function* () { return store[key]; }),
set: (key, value) => __awaiter(void 0, void 0, void 0, function* () {
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 = yield callApi({ names: ['casey'] });
const result2 = yield callApi({ names: ['chloe', 'charlotte'] });
const result3 = yield 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', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const store = {};
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ names }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(names);
return names;
}), {
cache: {
get: (key) => __awaiter(void 0, void 0, void 0, function* () { return store[key]; }),
set: (key, value) => __awaiter(void 0, void 0, void 0, function* () {
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 = yield callApi({ names: ['casey'] });
const result2 = yield callApi({ names: ['chloe', 'charlotte'] });
const result3 = yield callApi({ names: ['casey'] });
const result4 = yield 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);
}));
it('should use the same key serialization for the in-memory request deduplication cache', () => __awaiter(void 0, void 0, void 0, function* () {
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(({ name }) => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(name);
return name;
}), {
cache: (0, createExampleCache_1.createExampleAsyncCache)().cache,
serialize: {
key: ({ forInput }) => forInput[0].name.slice(0, 1), // serialize to only the first letter of the name arg
},
});
// create an object that for sure cant be serialized
const objA = { name: 'dog', refs: undefined };
const objB = { name: 'cat', refs: objA };
objA.refs = objB; // creates a cyclical reference -> can't be serialized
try {
JSON.stringify(objA);
throw new Error('should not reach here');
}
catch (error) {
if (!(error instanceof Error))
throw error;
expect(error.message).toContain('Converting circular structure to JSON');
}
// call the fn and prove that it didn't throw an error due to not being able to serialize the `unserializableObject` - it shouldn't have attempted since the custom serialization fn ignores it
yield callApi({ name: 'casey', unserializableObject: objA }); // no error
}));
});
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, withSimpleCachingAsync_1.withSimpleCachingAsync)(() => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(1);
return undefined; // return undefined each time
}), {
cache: (0, createExampleCache_1.createExampleAsyncCache)().cache,
});
// call the fn a few times
const result1 = yield callApi();
const result2 = yield callApi();
const result3 = yield 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.createExampleAsyncCache)();
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(() => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(1);
return Math.random();
}), {
cache,
});
// call the fn a few times
const result1 = yield callApi();
const result2 = yield callApi();
const result3 = yield 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 = yield 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, withSimpleCachingAsync_1.withSimpleCachingAsync)(() => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(1);
return null; // return null each time
}), { cache: (0, createExampleCache_1.createExampleAsyncCache)().cache });
// call the fn a few times
const result1 = yield callApi();
const result2 = yield callApi();
const result3 = yield 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 secondsUntilExpiration option correctly', () => __awaiter(void 0, void 0, void 0, function* () {
const { cache, store } = (0, createExampleCache_1.createExampleAsyncCache)();
// define an example fn
const apiCalls = [];
const callApi = (0, withSimpleCachingAsync_1.withSimpleCachingAsync)(() => __awaiter(void 0, void 0, void 0, function* () {
apiCalls.push(1);
return Math.random();
}), {
cache,
expiration: { seconds: 3 }, // wait three seconds until expiration
});
// call the fn
yield callApi();
// confirm that it passed the secondsUntilExpiration through to the cache
expect(store['[]']).toMatchObject({
options: { expiration: { seconds: 3 } },
});
}));
});
});
//# sourceMappingURL=withSimpleCachingAsync.test.js.map