diff --git a/ghost/adapter-cache-memory-ttl/.eslintrc.js b/ghost/adapter-cache-memory-ttl/.eslintrc.js new file mode 100644 index 0000000000..c9c1bcb522 --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/adapter-cache-memory-ttl/README.md b/ghost/adapter-cache-memory-ttl/README.md new file mode 100644 index 0000000000..724ce43939 --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/README.md @@ -0,0 +1,23 @@ +# Adapter Cache Memory Ttl + +Cache adapter with in-memory storage with TTL functionality + + +## Usage + + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + diff --git a/ghost/adapter-cache-memory-ttl/index.js b/ghost/adapter-cache-memory-ttl/index.js new file mode 100644 index 0000000000..ed5ff430f6 --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/adapter-cache-memory-ttl'); diff --git a/ghost/adapter-cache-memory-ttl/lib/adapter-cache-memory-ttl.js b/ghost/adapter-cache-memory-ttl/lib/adapter-cache-memory-ttl.js new file mode 100644 index 0000000000..2727096eba --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/lib/adapter-cache-memory-ttl.js @@ -0,0 +1,54 @@ +const TTLCache = require('@isaacs/ttlcache'); +const Base = require('@tryghost/adapter-base-cache'); + +/** + * Cache adapter compatible wrapper around TTLCache + * Distinct features of this cache adapter: + * - it is in-memory only + * - it supports time-to-live (TTL) + * - it supports a max number of items + */ +class MemoryTTL extends Base { + #cache; + + /** + * + * @param {Object} [deps] + * @param {Number} [deps.max] - The max number of items to keep in the cache. + * @param {Number} [deps.ttl] - The max time in ms to store items + */ + constructor({max = Infinity, ttl = Infinity} = {}) { + super(); + + this.#cache = new TTLCache({max, ttl}); + } + + get(key) { + return this.#cache.get(key); + } + + /** + * + * @param {String} key + * @param {*} value + * @param {Object} [options] + * @param {Number} [options.ttl] + */ + set(key, value, {ttl} = {}) { + this.#cache.set(key, value, {ttl}); + } + + reset() { + this.#cache.clear(); + } + + /** + * Helper method to assist "getAll" type of operations + * @returns {Array} all keys present in the cache + */ + keys() { + return [...this.#cache.keys()]; + } +} + +module.exports = MemoryTTL; diff --git a/ghost/adapter-cache-memory-ttl/package.json b/ghost/adapter-cache-memory-ttl/package.json new file mode 100644 index 0000000000..bb549e08ad --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tryghost/adapter-cache-memory-ttl", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/adapter-cache-memory-ttl", + "author": "Ghost Foundation", + "private": true, + "main": "index.js", + "scripts": { + "dev": "echo \"Implement me!\"", + "test:unit": "NODE_ENV=testing c8 --all --check-coverage --reporter text --reporter cobertura mocha './test/**/*.test.js'", + "test": "yarn test:unit", + "lint:code": "eslint *.js lib/ --ext .js --cache", + "lint": "yarn lint:code && yarn lint:test", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache" + }, + "files": [ + "index.js", + "lib" + ], + "devDependencies": { + "c8": "7.13.0", + "mocha": "10.2.0", + "sinon": "15.0.1" + }, + "dependencies": { + "@isaacs/ttlcache": "1.2.1" + } +} diff --git a/ghost/adapter-cache-memory-ttl/test/.eslintrc.js b/ghost/adapter-cache-memory-ttl/test/.eslintrc.js new file mode 100644 index 0000000000..829b601eb0 --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/test/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/adapter-cache-memory-ttl/test/adapter-cache-memory-ttl.test.js b/ghost/adapter-cache-memory-ttl/test/adapter-cache-memory-ttl.test.js new file mode 100644 index 0000000000..85a919b2af --- /dev/null +++ b/ghost/adapter-cache-memory-ttl/test/adapter-cache-memory-ttl.test.js @@ -0,0 +1,100 @@ +const assert = require('assert'); +const MemoryTTLCache = require('../index'); + +const sleep = ms => ( + new Promise((resolve) => { + setTimeout(resolve, ms); + }) +); + +describe('Cache Adapter In Memory with Time To Live', function () { + it('Can initialize a cache instance', function () { + const cache = new MemoryTTLCache(); + assert.ok(cache); + }); + + describe('get', function () { + it('Can get a value from the cache', async function () { + const cache = new MemoryTTLCache({}); + cache.set('a', 'b'); + assert.equal(cache.get('a'), 'b', 'should get the value from the cache'); + + await sleep(100); + + assert.equal(cache.get('a'), 'b', 'should get the value from the cache after some time'); + }); + + it('Can get a value from the cache before TTL kicks in', async function () { + const cache = new MemoryTTLCache({ttl: 150}); + cache.set('a', 'b'); + assert.equal(cache.get('a'), 'b', 'should get the value from the cache'); + + await sleep(100); + + assert.equal(cache.get('a'), 'b', 'should get the value from the cache before TTL time'); + + // NOTE: 100 + 100 = 200, which is more than 150 TTL + await sleep(100); + + assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after TTL time'); + }); + }); + + describe('set', function () { + it('Can set a value in the cache', async function () { + const cache = new MemoryTTLCache({ttl: 150}); + + cache.set('a', 'b'); + + assert.equal(cache.get('a'), 'b', 'should get the value from the cache'); + + await sleep(100); + + assert.equal(cache.get('a'), 'b', 'should get the value from the cache after time < TTL'); + + await sleep(100); + + assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after TTL time'); + }); + + it('Can override TTL time', async function () { + const cache = new MemoryTTLCache({ttl: 150}); + + cache.set('a', 'b', {ttl: 99}); + + assert.equal(cache.get('a'), 'b', 'should get the value from the cache'); + + await sleep(100); + + assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after TTL time'); + }); + }); + + describe('reset', function () { + it('Can reset the cache', async function () { + const cache = new MemoryTTLCache({ttl: 150}); + + cache.set('a', 'b'); + cache.set('c', 'd'); + + assert.equal(cache.get('a'), 'b', 'should get the value from the cache'); + assert.equal(cache.get('c'), 'd', 'should get the value from the cache'); + + cache.reset(); + + assert.equal(cache.get('a'), undefined, 'should NOT get the value from the cache after reset'); + assert.equal(cache.get('c'), undefined, 'should NOT get the value from the cache after reset'); + }); + }); + + describe('keys', function () { + it('Can get all keys from the cache', async function () { + const cache = new MemoryTTLCache({ttl: 200}); + + cache.set('a', 'b'); + cache.set('c', 'd'); + + assert.deepEqual(cache.keys(), ['a', 'c'], 'should get all keys from the cache'); + }); + }); +}); diff --git a/ghost/core/core/server/adapters/cache/MemoryTTL.js b/ghost/core/core/server/adapters/cache/MemoryTTL.js new file mode 100644 index 0000000000..302443968a --- /dev/null +++ b/ghost/core/core/server/adapters/cache/MemoryTTL.js @@ -0,0 +1,3 @@ +const TTLMemoryCache = require('@tryghost/adapter-cache-memory-ttl'); + +module.exports = TTLMemoryCache; diff --git a/yarn.lock b/yarn.lock index 3c320e55bc..d88011946d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2830,6 +2830,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@isaacs/ttlcache@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.2.1.tgz#07f54e31ee2dde9f0d2608fe3707f358596825e2" + integrity sha512-6hUKl0TcmeenJXitePBS/vPn1l/C8+sO4vvSmRh/hW4CeBm+QselPM6AyiM7WON6jApouCJGUfHYbaNObcMFrQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"