UNPKG

with-simple-caching

Version:

A wrapper that makes it simple to add caching to any function

310 lines 15.8 kB
"use strict"; 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