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
JavaScript
"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