diff --git a/README.md b/README.md index 2f36045..3540a28 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,12 @@ This method allows you to remove an item from the cache. If the ID does not exis This method returns a promise that will resolve when the item has been removed from the cache. +### cache.clear([settings]) + +This method allows you to clear all the items from the cache. The settings parameter is an optional object that you can pass in with a `internalCacheOnly` property that if set to true, won't call the clear method on the plugins. + +This method returns a promise that will resolve when the items have been cleared from the cache. + ### cache.fetch(id, retrieveFunction) This method allows you to get an item from the cache then fall back to a retrieveFunction if the item is not in the cache. If you call `cache.fetch` multiple times before the `retrieveFunction` has completed, it will only call the `retrieveFunction` once, and resolve all the promises after that one `retrieveFunction` has completed. diff --git a/lib/index.js b/lib/index.js index dbd0071..9e4400b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,6 +3,7 @@ debug.get = require("debug")("HeapStash:get"); debug.fetch = require("debug")("HeapStash:fetch"); debug.put = require("debug")("HeapStash:put"); debug.remove = require("debug")("HeapStash:remove"); +debug.clear = require("debug")("HeapStash:clear"); class HeapStash { constructor(settings = {}) { @@ -191,6 +192,20 @@ class HeapStash { debug.remove("Not removing item from plugins since internalCacheOnly is set to false."); } } + async clear(settings = {}) { + debug.clear("Clearing cache"); + + this._.internalcache = {}; + this._.internalcachearray = []; + + if (!settings.internalCacheOnly) { + debug.clear("Clearing cache from plugins"); + await Promise.all(this.plugins.map((plugin) => plugin.run("clear")())); + debug.clear("Done clearing cache in plugins."); + } else { + debug.clear("Not clearing cache from plugins since internalCacheOnly is set to false."); + } + } } module.exports = HeapStash; diff --git a/lib/plugin/DynamoDB.js b/lib/plugin/DynamoDB.js index f5a4527..710b82b 100644 --- a/lib/plugin/DynamoDB.js +++ b/lib/plugin/DynamoDB.js @@ -57,6 +57,20 @@ module.exports = (settings) => { "TableName": plugin._.tableName }).promise(); }; + plugin.tasks.clear = async () => { + let items = [], lastEvaluatedKey; + do { + const obj = {"TableName": plugin._.tableName}; + if (lastEvaluatedKey) { + obj.ExclusiveStartKey = lastEvaluatedKey; + } + const res = await plugin._.dynamodb.scan(obj).promise(); + items = [...items, ...res.Items]; + lastEvaluatedKey = res.LastEvaluatedKey; + } while (lastEvaluatedKey); + + await Promise.all(items.map((item) => item.id).map((id) => plugin._.dynamodb.deleteItem({"Key": {id}, "TableName": plugin._.tableName}).promise())); + }; return plugin; }; diff --git a/lib/plugin/FileSystem.js b/lib/plugin/FileSystem.js index 4e6cbe1..c321fb1 100644 --- a/lib/plugin/FileSystem.js +++ b/lib/plugin/FileSystem.js @@ -45,6 +45,37 @@ module.exports = (settings) => { }); }); }; + filesystem.tasks.clear = () => { + return new Promise((resolve, reject) => { + fs.readdir(path.join(settings.path), async (err, files) => { + /* istanbul ignore next */ + if (err) { + reject(); + } + + function deleteFilePromise(file) { + return new Promise((resolve, reject) => { + fs.unlink(path.join(settings.path, file), (err) => { + /* istanbul ignore next */ + if (err) { + reject(); + } else { + resolve(); + } + }); + }); + } + + try { + await Promise.all(files.map((file) => deleteFilePromise(file))); + resolve(); + } catch (e) { + /* istanbul ignore next */ + reject(); + } + }); + }); + }; return filesystem; }; diff --git a/test/07_clear.js b/test/07_clear.js new file mode 100644 index 0000000..22ca1d2 --- /dev/null +++ b/test/07_clear.js @@ -0,0 +1,55 @@ +const HeapStash = require("../"); +const {expect} = require("chai"); + +describe("clear()", () => { + let cache; + beforeEach(() => cache = new HeapStash()); + + it("Should be a function", () => { + expect(cache.clear).to.be.a("function"); + }); + + it("Should clear item from cache", async () => { + cache._.internalcache["test"] = {"data": 123}; + cache._.internalcachearray.push("test"); + + await cache.clear(); + + expect(cache._.internalcache).to.eql({}); + expect(cache._.internalcachearray).to.eql([]); + }); + + it("Should clear items from cache", async () => { + (new Array(50).fill("a").map((a, index) => index + 1)).forEach((item) => { + cache._.internalcache[`test${item}`] = {"data": item * 50}; + cache._.internalcachearray.push(`test${item}`); + }); + + expect(cache._.internalcachearray.length).to.eql(50); + + await cache.clear(); + + expect(cache._.internalcache).to.eql({}); + expect(cache._.internalcachearray).to.eql([]); + }); + + it("Should fail silently if nothing in cache", async () => { + await cache.clear(); + + expect(cache._.internalcache).to.eql({}); + expect(cache._.internalcachearray).to.eql([]); + }); + + it("Should not call any plugins if internalCacheOnly is set to true", async () => { + let called = false; + const plugin = new HeapStash.Plugin(); + plugin.tasks.clear = () => { + called = true; + }; + cache.plugins.push(plugin); + + await cache.clear({"internalCacheOnly": true}); + + expect(called).to.be.false; + }); +}); diff --git a/test/plugins/02_FileSystem.js b/test/plugins/02_FileSystem.js index 655f504..fa0c865 100644 --- a/test/plugins/02_FileSystem.js +++ b/test/plugins/02_FileSystem.js @@ -107,4 +107,39 @@ describe("FileSystem", () => { expect(error).to.not.exist; }); }); + + describe("clear()", () => { + beforeEach(() => new Promise((resolve, reject) => fs.writeFile(path.join(__dirname, "tmp", "id"), JSON.stringify({"data": {"myitem": "Hello World"}}), (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }))); + + it("Should clear item from file system cache", async () => { + await cache.clear(); + + let error, data; + try { + data = fs.readFileSync(path.join(__dirname, "tmp", "id"), "utf8"); + } catch (e) { + error = e; + } + + expect(error.message).to.include("ENOENT: no such file or directory"); + expect(data).to.not.exist; + }); + + it("Should fail silently if no item in cache", async () => { + let error; + try { + await cache.clear(); + } catch (e) { + error = e; + } + + expect(error).to.not.exist; + }); + }); }); diff --git a/test/plugins/03_DynamoDB.js b/test/plugins/03_DynamoDB.js index 414a58c..e3dcb4f 100644 --- a/test/plugins/03_DynamoDB.js +++ b/test/plugins/03_DynamoDB.js @@ -205,4 +205,50 @@ describe("DynamoDB", function() { expect(error).to.not.exist; }); }); + + describe("clear()", () => { + beforeEach(() => dynamodb.putItem({ + "Item": AWS.DynamoDB.Converter.marshall({"id": "id", "data": {"myitem": "Hello World"}}), + "TableName": "TestTable" + }).promise()); + + it("Should clear item from DynamoDB cache", async () => { + await cache.clear(); + + const data = AWS.DynamoDB.Converter.unmarshall((await dynamodb.getItem({ + "Key": { + "id": { + "S": "id" + } + }, + "TableName": "TestTable" + }).promise()).Item); + + expect(data).to.eql({}); + }); + + it("Should clear items from DynamoDB cache", async () => { + const dataMap = {"myitem": "Hello World", "largestring": new Array(100000).fill("a").join("")}; + await Promise.all((new Array(15).fill("a").map((a, index) => index + 1)).map((item) => dynamodb.putItem({ + "Item": AWS.DynamoDB.Converter.marshall({"id": `id${item}`, dataMap}), + "TableName": "TestTable" + }).promise())); + + await cache.clear(); + + const data = (await dynamodb.scan({"TableName": "TestTable"}).promise()).Items; + expect(data).to.eql([]); + }); + + it("Should fail silently if no item in cache", async () => { + let error; + try { + await cache.clear(); + } catch (e) { + error = e; + } + + expect(error).to.not.exist; + }); + }); });