UNPKG

simple-on-disk-cache

Version:

A simple on-disk cache, supporting local and remote filesystem targets, with time based expiration policies.

265 lines 17 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 uni_time_1 = require("@ehmpathy/uni-time"); const fs_1 = require("fs"); const cache_1 = require("./cache"); jest.setTimeout(60 * 1000); describe('cache', () => { describe('mounted', () => { const directoryToPersistTo = { mounted: { path: `${__dirname}/__tmp__` } }; it('should be able to add an item to the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); yield set('meaning-of-life', '42'); })); it('should be able to get an item from the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); yield set('how-many-licks-does-it-take-to-get-to-the-center-of-a-tootsie-pop', '3'); const licks = yield get('how-many-licks-does-it-take-to-get-to-the-center-of-a-tootsie-pop'); expect(licks).toEqual('3'); })); it('should respect the default expiration for the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo, expiration: { seconds: 10 }, }); // we're gonna use this cache to keep track of the popcorn in the microwave - we should check more regularly since it changes quickly! yield set('how-popped-is-the-popcorn', 'not popped'); // prove that we recorded the value and its accessible immediately after setting const popcornStatus = yield get('how-popped-is-the-popcorn'); expect(popcornStatus).toEqual('not popped'); // prove that the value is still accessible after 9 seconds, since default ttl is 10 seconds yield (0, uni_time_1.sleep)(9 * 1000); const popcornStatusAfter9Sec = yield get('how-popped-is-the-popcorn'); expect(popcornStatusAfter9Sec).toEqual('not popped'); // still should say not popped // and prove that after a total of 9 seconds, the status is no longer in the cache yield (0, uni_time_1.sleep)(1 * 1000); // sleep 1 more second const popcornStatusAfter10Sec = yield get('how-popped-is-the-popcorn'); expect(popcornStatusAfter10Sec).toEqual(undefined); // no longer defined, since the default seconds until expiration was 15 })); it('should respect the item level expiration for the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); // remember, default expiration is greater than 1 min yield set('ice-cream-state', 'solid', { expiration: { seconds: 5 } }); // ice cream changes quickly in the heat! lets keep a quick eye on this // prove that we recorded the value and its accessible immediately after setting const iceCreamState = yield get('ice-cream-state'); expect(iceCreamState).toEqual('solid'); // prove that the value is still accessible after 4 seconds, since default ttl is 5 seconds yield (0, uni_time_1.sleep)(4 * 1000); const iceCreamStateAfter4Sec = yield get('ice-cream-state'); expect(iceCreamStateAfter4Sec).toEqual('solid'); // still should say solid // and prove that after a total of 5 seconds, the state is no longer in the cache yield (0, uni_time_1.sleep)(1 * 1000); // sleep 1 more second const iceCreamStateAfter5Sec = yield get('ice-cream-state'); expect(iceCreamStateAfter5Sec).toEqual(undefined); // no longer defined, since the item level seconds until expiration was 5 })); it('should consider secondsUntilExpiration of null or infinity as never expiring', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo, expiration: { seconds: 0 }, // expire immediately }); // prove that setting something to the cache with default state will have it expired immediately yield set('dory-memory', 'something'); // lets see if dory can remember something const doryMemory = yield get('dory-memory'); expect(doryMemory).toEqual(undefined); // its already gone! dang default expiration // prove that if we record the memory with expires-at null, it persists yield set('elephant-memory', 'something', { expiration: null, }); const elephantMemory = yield get('elephant-memory'); expect(elephantMemory).toEqual('something'); })); it('should return undefined if a key has never been cached', () => __awaiter(void 0, void 0, void 0, function* () { const { get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); const value = yield get('ghostie'); expect(value).toEqual(undefined); })); it('should save to disk the value json parsed, if parseable, to make it easier to observe when debugging', () => __awaiter(void 0, void 0, void 0, function* () { const { get, set } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); // set const key = 'city'; const value = JSON.stringify({ name: 'atlantis', galaxy: 'pegasus', code: 821, }); yield set(key, value); // check that in the file it was json parsed before stringified const contents = yield fs_1.promises.readFile([directoryToPersistTo.mounted.path, key].join('/'), { encoding: 'utf-8', }); const parsedContents = JSON.parse(contents); expect(parsedContents.deserializedForObservability).toEqual(true); expect(typeof parsedContents.value).not.toEqual('string'); // check that we can read the value const foundValue = yield get(key); expect(foundValue).toEqual(value); })); it('should expose the error on set, if a promise that resolves with an error was called to be set to the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); // define the value const key = 'surprise'; const expectedError = new Error('surprise!'); const value = Promise.reject(expectedError); // prove the error is thrown onSet try { yield set(key, value); throw new Error('should not reach here'); } catch (error) { expect(error).toEqual(expectedError); } // prove nothing was set into the cache for this key const fileExists = yield yield fs_1.promises .readFile([directoryToPersistTo.mounted.path, key].join('/'), { encoding: 'utf-8', }) .then(() => true) .catch((error) => { if (error.code === 'ENOENT') return false; throw error; // otherwise, something else is messed up }); expect(fileExists).toEqual(false); })); it('should support invalidation by setting a keys value to undefined', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); yield set('is-cereal-soup', 'yes'); const answer = yield get('is-cereal-soup'); expect(answer).toEqual('yes'); yield set('is-cereal-soup', undefined); const answerNow = yield get('is-cereal-soup'); expect(answerNow).toEqual(undefined); })); it('should keep accurate track of keys', () => __awaiter(void 0, void 0, void 0, function* () { // clear out the old keys, so that other tests dont affect the keycounting we want to do here yield fs_1.promises.unlink(`${directoryToPersistTo.mounted.path}/${cache_1.RESERVED_CACHE_KEY_FOR_VALID_KEYS}`); // create the cache const { set, keys } = (0, cache_1.createCache)({ directory: directoryToPersistTo, }); // check key is added when value is set yield set('meaning-of-life', '42'); const keys1 = yield keys(); expect(keys1.length).toEqual(1); expect(keys1[0]).toEqual('meaning-of-life'); // check that there are no duplicates when key value is updated yield set('meaning-of-life', '42.0'); const keys2 = yield keys(); expect(keys2.length).toEqual(1); expect(keys2[0]).toEqual('meaning-of-life'); // check that multiple keys can be set yield set('purpose-of-life', 'propagation'); const keys3 = yield keys(); expect(keys3.length).toEqual(2); expect(keys3[1]).toEqual('purpose-of-life'); // check that invalidation removes the key yield set('meaning-of-life', undefined); const keys4 = yield keys(); expect(keys4.length).toEqual(1); expect(keys4[0]).toEqual('purpose-of-life'); })); it('should prevent redundant disk.reads to maximize speed', () => __awaiter(void 0, void 0, void 0, function* () { // clear out the old keys, so that other tests dont affect the keycounting we want to do here yield fs_1.promises.unlink(`${directoryToPersistTo.mounted.path}/${cache_1.RESERVED_CACHE_KEY_FOR_VALID_KEYS}`); // spy on the readFile api const readFileSpy = jest.spyOn(fs_1.promises, 'readFile'); // create the cache const cacheFirst = (0, cache_1.createCache)({ directory: directoryToPersistTo, }); // set a value yield cacheFirst.set('meaning-of-life', '42'); // verify the expected number of disk reads expect(readFileSpy).toHaveBeenCalledTimes(1); // get the value const valueFirst = yield cacheFirst.get('meaning-of-life'); expect(valueFirst).toEqual('42'); // verify that we did not readFile any more times, since it should have been .set to memory already expect(readFileSpy).toHaveBeenCalledTimes(1); // now, create a new cache, to clear out the in memory cache const cacheSecond = (0, cache_1.createCache)({ directory: directoryToPersistTo, }); // get the value again const valueSecond = yield cacheSecond.get('meaning-of-life'); expect(valueSecond).toEqual('42'); // same value // verify that we read from disk once to find it, since it was not in memory expect(readFileSpy).toHaveBeenCalledTimes(3); // 2x get on read through // get the value again const valueThird = yield cacheSecond.get('meaning-of-life'); expect(valueThird).toEqual('42'); // same value // verify that we did not readFile any more times, since it should have been .set to memory already expect(readFileSpy).toHaveBeenCalledTimes(3); })); }); describe('s3', () => { const directoryToPersistTo = { s3: { bucket: 'ehmpathy-simple-on-disk-cache-test-bucket', prefix: 'test/integration/s3', }, }; it('should be able to add an item to the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); yield set('meaning-of-life', '42'); })); it('should be able to get an item from the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); yield set('how-many-licks-does-it-take-to-get-to-the-center-of-a-tootsie-pop', '3'); const licks = yield get('how-many-licks-does-it-take-to-get-to-the-center-of-a-tootsie-pop'); expect(licks).toEqual('3'); })); it('should respect the default expiration for the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo, expiration: { seconds: 10 }, }); // we're gonna use this cache to keep track of the popcorn in the microwave - we should check more regularly since it changes quickly! yield set('how-popped-is-the-popcorn', 'not popped'); // prove that we recorded the value and its accessible immediately after setting const popcornStatus = yield get('how-popped-is-the-popcorn'); expect(popcornStatus).toEqual('not popped'); // prove that the value is still accessible after 8 seconds, since default ttl is 10 seconds yield (0, uni_time_1.sleep)(8 * 1000); const popcornStatusAfter9Sec = yield get('how-popped-is-the-popcorn'); expect(popcornStatusAfter9Sec).toEqual('not popped'); // still should say not popped // and prove that after a total of 10 seconds, the status is no longer in the cache yield (0, uni_time_1.sleep)(2 * 1000); // sleep 2 more second const popcornStatusAfter10Sec = yield get('how-popped-is-the-popcorn'); expect(popcornStatusAfter10Sec).toEqual(undefined); // no longer defined, since the default seconds until expiration was 15 })); it('should respect the item level expiration for the cache', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); // remember, default expiration is greater than 1 min yield set('ice-cream-state', 'solid', { expiration: { seconds: 5 } }); // ice cream changes quickly in the heat! lets keep a quick eye on this // prove that we recorded the value and its accessible immediately after setting const iceCreamState = yield get('ice-cream-state'); expect(iceCreamState).toEqual('solid'); // prove that the value is still accessible after 4 seconds, since default ttl is 5 seconds yield (0, uni_time_1.sleep)(3 * 1000); const iceCreamStateAfter4Sec = yield get('ice-cream-state'); expect(iceCreamStateAfter4Sec).toEqual('solid'); // still should say solid // and prove that after a total of 5 seconds, the state is no longer in the cache yield (0, uni_time_1.sleep)(2 * 1000); // sleep 2 more second const iceCreamStateAfter5Sec = yield get('ice-cream-state'); expect(iceCreamStateAfter5Sec).toEqual(undefined); // no longer defined, since the item level seconds until expiration was 5 })); it('should return undefined if a key has never been cached', () => __awaiter(void 0, void 0, void 0, function* () { const { get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); const value = yield get('ghostie'); expect(value).toEqual(undefined); })); it('should support an async getter for the directory to persist to', () => __awaiter(void 0, void 0, void 0, function* () { const { set, get } = (0, cache_1.createCache)({ directory: directoryToPersistTo }); yield set('what-do-you-call-a-fake-noodle', 'an-impasta'); const answer = yield get('what-do-you-call-a-fake-noodle'); expect(answer).toEqual('an-impasta'); })); }); }); //# sourceMappingURL=cache.integration.test.js.map