Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transformer option #50

Merged
merged 6 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
26 changes: 20 additions & 6 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,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) {
Expand Down Expand Up @@ -281,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
}
Expand Down Expand Up @@ -334,10 +341,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 })
})