From 5f71fb82fe6519a37622a8c86a3b79aab4fbc4ae Mon Sep 17 00:00:00 2001 From: hmbrg Date: Tue, 7 Feb 2023 23:25:29 +0100 Subject: [PATCH 1/5] add transformer --- index.d.ts | 7 +++++ src/cache.js | 19 ++++++++++-- src/symbol.js | 3 +- test/transformer.test.js | 65 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 test/transformer.test.js diff --git a/index.d.ts b/index.d.ts index 376dba1..4d7bd18 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,6 +26,11 @@ interface StorageMemoryOptions { invalidation?: boolean; } +interface DataTransformer { + serialize: (data: any) => any; + deserialize: (data: any) => any; +} + type Events = { onDedupe?: (key: string) => void; onError?: (err: any) => void; @@ -63,6 +68,7 @@ declare function createCache( options?: { storage?: StorageInputRedis | StorageInputMemory; ttl?: number; + transformer?: DataTransformer; } & Events ): Cache; @@ -79,6 +85,7 @@ declare class Cache { name: string, opts: { storage?: StorageOptionsType; + transformer?: DataTransformer; ttl?: number; serialize?: (...args: any[]) => any; references?: (...args: any[]) => References | Promise; diff --git a/src/cache.js b/src/cache.js index 5f54608..92a0578 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,6 +1,6 @@ 'use strict' -const { kValues, kStorage, kStorages, kTTL, kOnDedupe, kOnError, kOnHit, kOnMiss, kStale } = require('./symbol') +const { kValues, kStorage, kStorages, kTransfromer, kTTL, kOnDedupe, kOnError, kOnHit, kOnMiss, kStale } = require('./symbol') const stringify = require('safe-stable-stringify') const createStorage = require('./storage') @@ -8,6 +8,7 @@ class Cache { /** * @param {!Object} opts * @param {!Storage} opts.storage - the storage to use + * @param {?Object} opts.transformer - the transformer to use * @param {?number} [opts.ttl=0] - in seconds; default is 0 seconds, so it only does dedupe without cache * @param {?function} opts.onDedupe * @param {?function} opts.onError @@ -51,6 +52,8 @@ class Cache { this[kStorages] = new Map() this[kStorages].set('_default', options.storage) + this[kTransfromer] = options.transformer + this[kTTL] = options.ttl || 0 this[kOnDedupe] = options.onDedupe || noop this[kOnError] = options.onError || noop @@ -64,6 +67,7 @@ class Cache { * @param {!string} name name of the function * @param {?Object} [opts] * @param {?Object} [opts.storage] storage to use; default is the main one + * @param {?Object} opts.transformer - the transformer to use * @param {?number} [opts.ttl] ttl for the results; default ttl is the one passed to the constructor * @param {?function} [opts.onDedupe] function to call on dedupe; default is the one passed to the constructor * @param {?function} [opts.onError] function to call on error; default is the one passed to the constructor @@ -119,8 +123,9 @@ class Cache { const onError = opts.onError || this[kOnError] const onHit = opts.onHit || this[kOnHit] const onMiss = opts.onMiss || this[kOnMiss] + const transformer = opts.transformer || this[kTransfromer] - const wrapper = new Wrapper(func, name, serialize, references, storage, ttl, onDedupe, onError, onHit, onMiss, stale) + const wrapper = new Wrapper(func, name, serialize, references, storage, transformer, ttl, onDedupe, onError, onHit, onMiss, stale) this[kValues][name] = wrapper this[name] = wrapper.add.bind(wrapper) @@ -187,6 +192,7 @@ class Wrapper { * @param {function} serialize * @param {function} references * @param {Storage} storage + * @param {Object} transformer * @param {number} ttl * @param {function} onDedupe * @param {function} onError @@ -194,7 +200,7 @@ class Wrapper { * @param {function} onMiss * @param {stale} ttl */ - constructor (func, name, serialize, references, storage, ttl, onDedupe, onError, onHit, onMiss, stale) { + constructor (func, name, serialize, references, storage, transformer, ttl, onDedupe, onError, onHit, onMiss, stale) { this.dedupes = new Map() this.func = func this.name = name @@ -202,6 +208,7 @@ class Wrapper { this.references = references this.storage = storage + this.transformer = transformer this.ttl = ttl this.onDedupe = onDedupe this.onError = onError @@ -334,10 +341,16 @@ class Wrapper { } async get (key) { + if (this.transformer) { + return this.transformer.deserialize(this.storage.get(key)) + } return this.storage.get(key) } async set (key, value, ttl, references) { + if (this.transformer) { + value = this.transformer.serialize(value) + } return this.storage.set(key, value, ttl, references) } diff --git a/src/symbol.js b/src/symbol.js index 32457a5..8c8ff53 100644 --- a/src/symbol.js +++ b/src/symbol.js @@ -3,6 +3,7 @@ const kValues = Symbol('values') const kStorage = Symbol('kStorage') const kStorages = Symbol('kStorages') +const kTransfromer = Symbol('kTransformer') const kTTL = Symbol('kTTL') const kOnDedupe = Symbol('kOnDedupe') const kOnError = Symbol('kOnError') @@ -10,4 +11,4 @@ const kOnHit = Symbol('kOnHit') const kOnMiss = Symbol('kOnMiss') const kStale = Symbol('kStale') -module.exports = { kValues, kStorage, kStorages, kTTL, kOnDedupe, kOnError, kOnHit, kOnMiss, kStale } +module.exports = { kValues, kStorage, kStorages, kTransfromer, kTTL, kOnDedupe, kOnError, kOnHit, kOnMiss, kStale } diff --git a/test/transformer.test.js b/test/transformer.test.js new file mode 100644 index 0000000..58c8ecb --- /dev/null +++ b/test/transformer.test.js @@ -0,0 +1,65 @@ +'use strict' + +const { test, before, teardown } = require('tap') +const Redis = require('ioredis') + +const createStorage = require('../src/storage') +const { Cache } = require('../') + +let redisClient +before(async (t) => { + redisClient = new Redis() +}) + +teardown(async (t) => { + await redisClient.quit() +}) + +test('should handle a custom transformer to store and get data per cache', async function (t) { + t.plan(4) + + const cache = new Cache({ + storage: createStorage(), + transformer: { + serialize: (value) => { + t.pass('serialize called') + return JSON.stringify(value) + }, + deserialize: (value) => { + t.pass('deserialize called') + return JSON.parse(value) + } + } + }) + + cache.define('fetchSomething', async (query, cacheKey) => { + return { k: query } + }) + + t.same(await cache.fetchSomething(42), { k: 42 }) +}) + +test('should handle a custom transformer to store and get data per define', async function (t) { + t.plan(4) + + const cache = new Cache({ + storage: createStorage() + }) + + cache.define('fetchSomething', { + transformer: { + serialize: (value) => { + t.pass('serialize called') + return JSON.stringify(value) + }, + deserialize: (value) => { + t.pass('deserialize called') + return JSON.parse(value) + } + } + }, async (query, cacheKey) => { + return { k: query } + }) + + t.same(await cache.fetchSomething(42), { k: 42 }) +}) From f0e6771059759ee7cd611a2162992e6b40a145b0 Mon Sep 17 00:00:00 2001 From: hmbrg Date: Sun, 12 Feb 2023 04:18:33 +0100 Subject: [PATCH 2/5] add await to transformer deserialize --- src/cache.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cache.js b/src/cache.js index 92a0578..3565de9 100644 --- a/src/cache.js +++ b/src/cache.js @@ -255,7 +255,7 @@ class Wrapper { async wrapFunction (args, key) { const storageKey = this.getStorageKey(key) if (this.ttl > 0 || typeof this.ttl === 'function') { - let data = this.storage.get(storageKey) + let data = this.get(storageKey) if (data && typeof data.then === 'function') { data = await data } if (data !== undefined) { @@ -342,7 +342,7 @@ class Wrapper { async get (key) { if (this.transformer) { - return this.transformer.deserialize(this.storage.get(key)) + return await this.transformer.deserialize(await this.storage.get(key)) } return this.storage.get(key) } From 7e756f16cf3244ad5db81489a9d8320184c9f95b Mon Sep 17 00:00:00 2001 From: hmbrg Date: Sun, 12 Feb 2023 04:18:47 +0100 Subject: [PATCH 3/5] add transformer documentation --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index b1aee4e..706c690 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,25 @@ Options: ```js createCache({ storage: { type: 'redis', options: { client: new Redis(), invalidation: { referencesTTL: 60 } } } }) ``` +* `transformer`: the transformer to used to serialize and deserialize the cache entries. + It must be an object with the following methods: + * `serialize`: a function that receives the result of the original function and returns a serializable object. + * `deserialize`: a function that receives the serialized object and returns the original result. + + * Default is `undefined`, so the default transformer is used. + + Example + + ```js + import superjson from 'superjson'; + + const cache = createCache({ + transformer: { + serialize: (result) => superjson.serialize(result), + deserialize: (serialized) => superjson.deserialize(serialized), + } + }) + ``` ### `cache.define(name[, opts], original(arg, cacheKey))` @@ -101,6 +120,7 @@ Options: * `onHit`: a function that is called every time there is a hit in the cache. * `onMiss`: a function that is called every time the result is not in the cache. * `storage`: the storage to use, same as above. It's possible to specify different storages for each defined function for fine-tuning. +* `transformer`: the transformer to used to serialize and deserialize the cache entries. It's possible to specify different transformers for each defined function for fine-tuning. * `references`: sync or async function to generate references, it receives `(args, key, result)` from the defined function call and must return an array of strings or falsy; see [invalidation](#invalidation) to know how to use them. Example 1 From 56e91b97a6beea22a867ddcb9cd347cac86d6a2d Mon Sep 17 00:00:00 2001 From: hmbrg Date: Mon, 13 Feb 2023 03:25:41 +0100 Subject: [PATCH 4/5] use correct get method in wrapper & fix transformer tests --- src/cache.js | 9 +++++---- test/transformer.test.js | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/cache.js b/src/cache.js index 3565de9..a28b2e4 100644 --- a/src/cache.js +++ b/src/cache.js @@ -288,7 +288,7 @@ class Wrapper { } if (!this.references) { - let p = this.storage.set(storageKey, result, ttl) + let p = this.set(storageKey, result, ttl) if (p && typeof p.then === 'function') { p = await p } @@ -341,10 +341,11 @@ class Wrapper { } async get (key) { - if (this.transformer) { - return await this.transformer.deserialize(await this.storage.get(key)) + const data = await this.storage.get(key) + if (this.transformer && !!data) { + return await this.transformer.deserialize(data) } - return this.storage.get(key) + return data } async set (key, value, ttl, references) { diff --git a/test/transformer.test.js b/test/transformer.test.js index 58c8ecb..a03b879 100644 --- a/test/transformer.test.js +++ b/test/transformer.test.js @@ -16,9 +16,8 @@ teardown(async (t) => { }) test('should handle a custom transformer to store and get data per cache', async function (t) { - t.plan(4) - const cache = new Cache({ + ttl: 1000, storage: createStorage(), transformer: { serialize: (value) => { @@ -37,16 +36,16 @@ test('should handle a custom transformer to store and get data per cache', async }) t.same(await cache.fetchSomething(42), { k: 42 }) + t.same(await cache.fetchSomething(42), { k: 42 }) }) test('should handle a custom transformer to store and get data per define', async function (t) { - t.plan(4) - const cache = new Cache({ storage: createStorage() }) cache.define('fetchSomething', { + ttl: 1000, transformer: { serialize: (value) => { t.pass('serialize called') @@ -62,4 +61,5 @@ test('should handle a custom transformer to store and get data per define', asyn }) t.same(await cache.fetchSomething(42), { k: 42 }) + t.same(await cache.fetchSomething(42), { k: 42 }) }) From dc8195f352b2d2186965b296625cbe2989544441 Mon Sep 17 00:00:00 2001 From: hmbrg Date: Mon, 13 Feb 2023 15:51:16 +0100 Subject: [PATCH 5/5] fix coverage --- src/cache.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/cache.js b/src/cache.js index a28b2e4..94f613f 100644 --- a/src/cache.js +++ b/src/cache.js @@ -255,8 +255,7 @@ class Wrapper { async wrapFunction (args, key) { const storageKey = this.getStorageKey(key) if (this.ttl > 0 || typeof this.ttl === 'function') { - let data = this.get(storageKey) - if (data && typeof data.then === 'function') { data = await data } + const data = await this.get(storageKey) if (data !== undefined) { this.onHit(key) @@ -288,10 +287,7 @@ class Wrapper { } if (!this.references) { - let p = this.set(storageKey, result, ttl) - if (p && typeof p.then === 'function') { - p = await p - } + await this.set(storageKey, result, ttl) return result }