Skip to content

Commit

Permalink
Add transformer option (#50)
Browse files Browse the repository at this point in the history
* add transformer

* add await to transformer deserialize

* add transformer documentation

* use correct get method in wrapper & fix transformer tests

* fix coverage
  • Loading branch information
hmbrg committed Feb 14, 2023
1 parent 341be35 commit a2d2236
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 11 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))`

Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +68,7 @@ declare function createCache(
options?: {
storage?: StorageInputRedis | StorageInputMemory;
ttl?: number;
transformer?: DataTransformer;
} & Events
): Cache;

Expand All @@ -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<References>;
Expand Down
30 changes: 20 additions & 10 deletions src/cache.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'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')

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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -187,21 +192,23 @@ class Wrapper {
* @param {function} serialize
* @param {function} references
* @param {Storage} storage
* @param {Object} transformer
* @param {number} ttl
* @param {function} onDedupe
* @param {function} onError
* @param {function} onHit
* @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
this.serialize = serialize
this.references = references

this.storage = storage
this.transformer = transformer
this.ttl = ttl
this.onDedupe = onDedupe
this.onError = onError
Expand Down Expand Up @@ -248,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.storage.get(storageKey)
if (data && typeof data.then === 'function') { data = await data }
const data = await this.get(storageKey)

if (data !== undefined) {
this.onHit(key)
Expand Down Expand Up @@ -281,10 +287,7 @@ class Wrapper {
}

if (!this.references) {
let p = this.storage.set(storageKey, result, ttl)
if (p && typeof p.then === 'function') {
p = await p
}
await this.set(storageKey, result, ttl)
return result
}

Expand Down Expand Up @@ -334,10 +337,17 @@ class Wrapper {
}

async get (key) {
return this.storage.get(key)
const data = await this.storage.get(key)
if (this.transformer && !!data) {
return await this.transformer.deserialize(data)
}
return data
}

async set (key, value, ttl, references) {
if (this.transformer) {
value = this.transformer.serialize(value)
}
return this.storage.set(key, value, ttl, references)
}

Expand Down
3 changes: 2 additions & 1 deletion src/symbol.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
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')
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 }
65 changes: 65 additions & 0 deletions test/transformer.test.js
Original file line number Diff line number Diff line change
@@ -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) {
const cache = new Cache({
ttl: 1000,
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 })
t.same(await cache.fetchSomething(42), { k: 42 })
})

test('should handle a custom transformer to store and get data per define', async function (t) {
const cache = new Cache({
storage: createStorage()
})

cache.define('fetchSomething', {
ttl: 1000,
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 })
t.same(await cache.fetchSomething(42), { k: 42 })
})

0 comments on commit a2d2236

Please sign in to comment.