profile
viewpoint
Matteo Collina mcollina @nearform In the clouds above Italy Technical Director @nearform, TSC member @nodejs, IoT Expert, Conference Speaker, Ph.D.

davidmarkclements/0x 2306

🔥 single-command flamegraph profiling 🔥

delvedor/find-my-way 940

A crazy fast HTTP router

davidmarkclements/rfdc 478

Really Fast Deep Clone

delvedor/fastify-example 332

This project is a small but feature complete application build with Fastify and Svelte, and it aims to show all the core concepts of Fastify, best practices, and recommendations.

davidmarkclements/v8-perf 273

Exploring v8 performance characteristics in Node across v8 versions 5.1, 5.8, 5.9, 6.0 and 6.1

davidmarkclements/fast-safe-stringify 267

Safely and quickly serialize JavaScript objects

davidmarkclements/overload-protection 184

Load detection and shedding capabilities for http, express, restify and koa

davidmarkclements/fast-redact 176

very fast object redaction

PullRequestReviewEvent
PullRequestReviewEvent

pull request commentmercurius-js/mercurius

Extend regression fix in handling bad json to include custom context

@mcollina I am not sure if context (custom and/or default) is needed in case when custom error handler has been used (as in is such case error formatter is not called at all with my understanding?)

I think it's needed. Folks would expect it to be there.

wiktor-obrebski

comment created time in 4 hours

PullRequestReviewEvent

Pull request review commentfastify/fastify

remove app.use and fix middleware tests

-<h1 align="center">Fastify</h1>

There are links to this document, they should be removed.

genzyy

comment created time in 7 hours

Pull request review commentfastify/fastify

remove app.use and fix middleware tests

 await fastify.register(require('fastify-express')); fastify.use(require('cors')()); ``` +**v4:**++Starting v4 we have deprecated the use of `.use`. Both `middie` and `fastify-express` will still be there and maintained.

Please start a Migration-Guide-V4.md file.

genzyy

comment created time in 7 hours

PullRequestReviewEvent
PullRequestReviewEvent
PullRequestReviewEvent

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

 'use strict' -const kValues = require('./symbol')+const { kValues, kStorage, kTTL, kOnDedupe, kOnHit, kOnMiss } = require('./symbol') const stringify = require('safe-stable-stringify')-const LRUCache = require('mnemonist/lru-cache')+const joi = require('joi') -const kCacheSize = Symbol('kCacheSize')-const kTTL = Symbol('kTTL')-const kOnHit = Symbol('kOnHit')+const cacheOptionsValidation = joi.object({+  storage: joi.object().required(),+  ttl: joi.number().integer().min(0).default(0),+  onDedupe: joi.func().default(() => noop),+  onHit: joi.func().default(() => noop),+  onMiss: joi.func().default(() => noop)+})  class Cache {+  /**+   * @param {!Object} opts+   * @param {!Storage} opts.storage - the storage 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.onHit+   * @param {?function} opts.onMiss+   */   constructor (opts) {-    opts = opts || {}+    const { value: options, error } = cacheOptionsValidation.validate(opts || {})+    if (error) { throw error }+     this[kValues] = {}-    this[kCacheSize] = opts.cacheSize || 1024-    this[kTTL] = opts.ttl || 0-    this[kOnHit] = opts.onHit || noop+    this[kStorage] = options.storage+    this[kTTL] = options.ttl+    this[kOnDedupe] = options.onDedupe+    this[kOnHit] = options.onHit+    this[kOnMiss] = options.onMiss   } -  define (key, opts, func) {+  /**+   * add a new function to dedupe (and cache)+   * @param {!string} name name of the function+   * @param {?Object} [opts]+   * @param {?function} [opts.storage] storage to use; default is the one passed to the constructor+   * @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.onHit] function to call on hit; default is the one passed to the constructor+   * @param {?function} [opts.onMiss] function to call on miss; default is the one passed to the constructor+   * @param {?function} [opts.serialize] custom function to serialize the arguments of `func`, in order to create the key for deduping and caching+   * @param {?function} [opts.references] function to generate references+   * @param {!function} func the function to dedupe (and cache)+   **/+  define (name, opts, func) {     if (typeof opts === 'function') {       func = opts       opts = {}     } -    if (key && this[key]) {-      throw new Error(`${key} is already defined in the cache or it is a forbidden name`)+    if (name && this[name]) {+      throw new Error(`${name} is already defined in the cache or it is a forbidden name`)     }      opts = opts || {}      if (typeof func !== 'function') {-      throw new TypeError(`Missing the function parameter for '${key}'`)+      throw new TypeError(`Missing the function parameter for '${name}'`)     }      const serialize = opts.serialize     if (serialize && typeof serialize !== 'function') {       throw new TypeError('serialize must be a function')     } -    const cacheSize = opts.cacheSize || this[kCacheSize]+    const references = opts.references+    if (references && typeof references !== 'function') {+      throw new TypeError('references must be a function')+    }++    // TODO doc we could even have a different storage for each key+    const storage = opts.storage || this[kStorage]     const ttl = opts.ttl || this[kTTL]+    const onDedupe = opts.onDedupe || this[kOnDedupe]     const onHit = opts.onHit || this[kOnHit]+    const onMiss = opts.onMiss || this[kOnMiss] -    const wrapper = new Wrapper(func, key, serialize, cacheSize, ttl, onHit)+    const wrapper = new Wrapper(func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) -    this[kValues][key] = wrapper-    this[key] = wrapper.add.bind(wrapper)+    this[kValues][name] = wrapper+    this[name] = wrapper.add.bind(wrapper)   } -  clear (key, value) {-    if (key) {-      this[kValues][key].clear(value)+  async clear (name, value) {+    if (name) {+      if (!this[kValues][name]) {+        throw new Error(`${name} is not defined in the cache`)+      }++      await this[kValues][name].clear(value)       return     } +    const clears = []     for (const wrapper of Object.values(this[kValues])) {-      wrapper.clear()+      clears.push(wrapper.clear())     }+    await Promise.all(clears)   }-} -let _currentSecond+  async get (name, key) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key? -function currentSecond () {-  if (_currentSecond !== undefined) {-    return _currentSecond+    return this[kValues][name].get(key)   }-  _currentSecond = Math.floor(Date.now() / 1000)-  setTimeout(_clearSecond, 1000).unref()-  return _currentSecond-} -function _clearSecond () {-  _currentSecond = undefined+  async set (name, key, value, ttl, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key, value, ttl, references?++    return this[kValues][name].set(key, value, ttl, references)+  }++  async invalidate (name, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate references?++    return this[kValues][name].invalidate(references)+  } }  class Wrapper {-  constructor (func, key, serialize, cacheSize, ttl, onHit) {-    this.ids = new LRUCache(cacheSize)-    this.error = null-    this.started = false+  // TODO signature+  constructor (func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) {+    this.dedupes = new Map()     this.func = func-    this.key = key+    this.name = name     this.serialize = serialize+    this.references = references++    this.storage = storage     this.ttl = ttl+    this.onDedupe = onDedupe     this.onHit = onHit-  }--  buildPromise (query, args, key) {-    query.promise = this.func(args, key)-    // we fork the promise chain on purpose-    const p = query.promise.catch(() => this.ids.set(key, undefined))-    if (this.ttl > 0) {-      query.cachedOn = currentSecond()-    } else {-      // clear the cache if there is no TTL-      p.then(() => this.ids.set(key, undefined))-    }+    this.onMiss = onMiss   }    getKey (args) {     const id = this.serialize ? this.serialize(args) : args     return typeof id === 'string' ? id : stringify(id)   } +  getStorageKey (key) {+    return `${this.name}~${key}`+  }++  getStorageName () {+    return `${this.name}~`+  }+   add (args) {     const key = this.getKey(args)-    const onHit = this.onHit -    let query = this.ids.get(key)+    let query = this.dedupes.get(key)     if (!query) {       query = new Query()       this.buildPromise(query, args, key)-      this.ids.set(key, query)-    } else if (this.ttl > 0) {-      onHit()-      if (currentSecond() - query.cachedOn > this.ttl) {-        // restart-        this.buildPromise(query, args, key)-      }+      this.dedupes.set(key, query)     } else {-      onHit()+      this.onDedupe(key)     }      return query.promise   } -  clear (value) {+  /**+   * wrap the original func to sync storage+   */+  async wrapFunction (args, key) {+    const storageKey = this.getStorageKey(key)+    const data = await this.storage.get(storageKey)

You can do the following:

let data = this.storage.get(storageKey)
if (data && tyepof data.then === 'function') {
  data = await data
} 
simone-sanfratello

comment created time in 9 hours

PullRequestReviewEvent

Pull request review commentnodejs/undici

feat: BodyReadable.dump

 The `RequestOptions.method` property should not be value `'CONNECT'`. - `body` - `bodyUsed` +`body` contains the following additional extentions:++- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket.

Please include the default. I think we could define in in bytes, so use a multiple of 2

ronag

comment created time in 9 hours

PullRequestReviewEvent
PullRequestReviewEvent

pull request commentfastify/fastify

remove app.use and fix middleware tests

Just remove it. Make sure of

  1. .use is not documented elsewhere
  2. middie and fastify-express as listed inside the core plugins.
genzyy

comment created time in 9 hours

issue commentfastify/fastify-passport

Local Strategy bad request

Please provide a reproducible example.

gasner

comment created time in 9 hours

Pull request review commentmercurius-js/mercurius

Fix regression in handling badly formed JSON

 module.exports = async function (app, opts) {   const errorFormatter = typeof opts.errorFormatter === 'function' ? opts.errorFormatter : defaultErrorFormatter    if (typeof opts.errorHandler === 'function') {-    app.setErrorHandler(opts.errorHandler)+    app.setErrorHandler((error, request, reply) => {+      const errorHandler = opts.errorHandler+      if (!request[kRequestContext]) {+        // Generate the context for this request+        request[kRequestContext] = { reply, app }

I just focused on fixing it asap. Open a fresh PR in case.

mcollina

comment created time in 13 hours

PullRequestReviewEvent

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')+const LRUCache = require('mnemonist/lru-cache')+const nullLogger = require('abstract-logging')+const StorageInterface = require('./interface')+const { findMatchingIndexes, findNotMatching, bsearchIndex } = require('../util')++const DEFAULT_CACHE_SIZE = 1024++const optionsValidation = joi.object({+  size: joi.number().integer().min(1).default(DEFAULT_CACHE_SIZE),+  log: joi.object().default(() => nullLogger),+  invalidation: joi.boolean().default(false)+})++/**+ * @typedef StorageMemoryOptions+ * @property {?number} [size=1024]+ * @property {?Logger} [log]+ * @property {?boolean} [invalidation=false]+ */++class StorageMemory extends StorageInterface {+  /**+   * in-memory storage+   * @param {StorageMemoryOptions} options+   */+  constructor (opts) {+    const { value: options, error } = optionsValidation.validate(opts || {})+    if (error) { throw error }++    super(options)+    this.size = options.size+    this.log = options.log+    this.invalidation = options.invalidation++    this.init()+  }++  init () {+    this.store = new LRUCache(this.size)++    if (!this.invalidation) {+      return+    }+    // key -> references, keys are strings, references are sorted array strings+    this.keysReferences = new Map()+    // same as above, but inverted+    this.referencesKeys = new Map()+  }++  /**+   * retrieve the value by key+   * @param {string} key+   * @returns {undefined|*} undefined if key not found or expired+   */+  async get (key) {+    this.log.debug({ msg: 'acd/storage/memory.get', key })++    const entry = this.store.get(key)+    if (entry) {+      this.log.debug({ msg: 'acd/storage/memory.get, entry', entry, now: now() })+      if (entry.start + entry.ttl > now()) {+        this.log.debug({ msg: 'acd/storage/memory.get, key is NOT expired', key, entry })+        return entry.value+      }+      this.log.debug({ msg: 'acd/storage/memory.get, key is EXPIRED', key, entry })++      // no need to wait for key to be removed++      setImmediate(() => this.remove(key))+    }+  }++  /**+   * set value by key+   * @param {string} key+   * @param {*} value+   * @param {?number} [ttl=0] - ttl in seconds; zero means key will not be stored+   * @param {?string[]} references+   */+  async set (key, value, ttl, references) {
 set (key, value, ttl, references) {
simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

 'use strict' -const kValues = require('./symbol')+const { kValues, kStorage, kTTL, kOnDedupe, kOnHit, kOnMiss } = require('./symbol') const stringify = require('safe-stable-stringify')-const LRUCache = require('mnemonist/lru-cache')+const joi = require('joi') -const kCacheSize = Symbol('kCacheSize')-const kTTL = Symbol('kTTL')-const kOnHit = Symbol('kOnHit')+const cacheOptionsValidation = joi.object({+  storage: joi.object().required(),+  ttl: joi.number().integer().min(0).default(0),+  onDedupe: joi.func().default(() => noop),+  onHit: joi.func().default(() => noop),+  onMiss: joi.func().default(() => noop)+})  class Cache {+  /**+   * @param {!Object} opts+   * @param {!Storage} opts.storage - the storage 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.onHit+   * @param {?function} opts.onMiss+   */   constructor (opts) {-    opts = opts || {}+    const { value: options, error } = cacheOptionsValidation.validate(opts || {})+    if (error) { throw error }+     this[kValues] = {}-    this[kCacheSize] = opts.cacheSize || 1024-    this[kTTL] = opts.ttl || 0-    this[kOnHit] = opts.onHit || noop+    this[kStorage] = options.storage+    this[kTTL] = options.ttl+    this[kOnDedupe] = options.onDedupe+    this[kOnHit] = options.onHit+    this[kOnMiss] = options.onMiss   } -  define (key, opts, func) {+  /**+   * add a new function to dedupe (and cache)+   * @param {!string} name name of the function+   * @param {?Object} [opts]+   * @param {?function} [opts.storage] storage to use; default is the one passed to the constructor+   * @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.onHit] function to call on hit; default is the one passed to the constructor+   * @param {?function} [opts.onMiss] function to call on miss; default is the one passed to the constructor+   * @param {?function} [opts.serialize] custom function to serialize the arguments of `func`, in order to create the key for deduping and caching+   * @param {?function} [opts.references] function to generate references+   * @param {!function} func the function to dedupe (and cache)+   **/+  define (name, opts, func) {     if (typeof opts === 'function') {       func = opts       opts = {}     } -    if (key && this[key]) {-      throw new Error(`${key} is already defined in the cache or it is a forbidden name`)+    if (name && this[name]) {+      throw new Error(`${name} is already defined in the cache or it is a forbidden name`)     }      opts = opts || {}      if (typeof func !== 'function') {-      throw new TypeError(`Missing the function parameter for '${key}'`)+      throw new TypeError(`Missing the function parameter for '${name}'`)     }      const serialize = opts.serialize     if (serialize && typeof serialize !== 'function') {       throw new TypeError('serialize must be a function')     } -    const cacheSize = opts.cacheSize || this[kCacheSize]+    const references = opts.references+    if (references && typeof references !== 'function') {+      throw new TypeError('references must be a function')+    }++    // TODO doc we could even have a different storage for each key+    const storage = opts.storage || this[kStorage]     const ttl = opts.ttl || this[kTTL]+    const onDedupe = opts.onDedupe || this[kOnDedupe]     const onHit = opts.onHit || this[kOnHit]+    const onMiss = opts.onMiss || this[kOnMiss] -    const wrapper = new Wrapper(func, key, serialize, cacheSize, ttl, onHit)+    const wrapper = new Wrapper(func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) -    this[kValues][key] = wrapper-    this[key] = wrapper.add.bind(wrapper)+    this[kValues][name] = wrapper+    this[name] = wrapper.add.bind(wrapper)   } -  clear (key, value) {-    if (key) {-      this[kValues][key].clear(value)+  async clear (name, value) {+    if (name) {+      if (!this[kValues][name]) {+        throw new Error(`${name} is not defined in the cache`)+      }++      await this[kValues][name].clear(value)       return     } +    const clears = []     for (const wrapper of Object.values(this[kValues])) {-      wrapper.clear()+      clears.push(wrapper.clear())     }+    await Promise.all(clears)   }-} -let _currentSecond+  async get (name, key) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key? -function currentSecond () {-  if (_currentSecond !== undefined) {-    return _currentSecond+    return this[kValues][name].get(key)   }-  _currentSecond = Math.floor(Date.now() / 1000)-  setTimeout(_clearSecond, 1000).unref()-  return _currentSecond-} -function _clearSecond () {-  _currentSecond = undefined+  async set (name, key, value, ttl, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key, value, ttl, references?++    return this[kValues][name].set(key, value, ttl, references)+  }++  async invalidate (name, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate references?++    return this[kValues][name].invalidate(references)+  } }  class Wrapper {-  constructor (func, key, serialize, cacheSize, ttl, onHit) {-    this.ids = new LRUCache(cacheSize)-    this.error = null-    this.started = false+  // TODO signature+  constructor (func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) {+    this.dedupes = new Map()     this.func = func-    this.key = key+    this.name = name     this.serialize = serialize+    this.references = references++    this.storage = storage     this.ttl = ttl+    this.onDedupe = onDedupe     this.onHit = onHit-  }--  buildPromise (query, args, key) {-    query.promise = this.func(args, key)-    // we fork the promise chain on purpose-    const p = query.promise.catch(() => this.ids.set(key, undefined))-    if (this.ttl > 0) {-      query.cachedOn = currentSecond()-    } else {-      // clear the cache if there is no TTL-      p.then(() => this.ids.set(key, undefined))-    }+    this.onMiss = onMiss   }    getKey (args) {     const id = this.serialize ? this.serialize(args) : args     return typeof id === 'string' ? id : stringify(id)   } +  getStorageKey (key) {+    return `${this.name}~${key}`+  }++  getStorageName () {+    return `${this.name}~`+  }+   add (args) {     const key = this.getKey(args)-    const onHit = this.onHit -    let query = this.ids.get(key)+    let query = this.dedupes.get(key)     if (!query) {       query = new Query()       this.buildPromise(query, args, key)-      this.ids.set(key, query)-    } else if (this.ttl > 0) {-      onHit()-      if (currentSecond() - query.cachedOn > this.ttl) {-        // restart-        this.buildPromise(query, args, key)-      }+      this.dedupes.set(key, query)     } else {-      onHit()+      this.onDedupe(key)     }      return query.promise   } -  clear (value) {+  /**+   * wrap the original func to sync storage+   */+  async wrapFunction (args, key) {+    const storageKey = this.getStorageKey(key)+    const data = await this.storage.get(storageKey)+    if (data !== undefined) {+      this.onHit(key)+      return data+    }++    this.onMiss(key)++    const result = await this.func(args, key)++    if (this.ttl < 1) {+      return result+    }++    if (!this.references) {+      await this.storage.set(storageKey, result, this.ttl)+      return result+    }++    const references = await this.references(args, key, result)+    // TODO validate references?+    await this.storage.set(storageKey, result, this.ttl, references)

Avoid await if possible.

simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')+const LRUCache = require('mnemonist/lru-cache')+const nullLogger = require('abstract-logging')+const StorageInterface = require('./interface')+const { findMatchingIndexes, findNotMatching, bsearchIndex } = require('../util')++const DEFAULT_CACHE_SIZE = 1024++const optionsValidation = joi.object({+  size: joi.number().integer().min(1).default(DEFAULT_CACHE_SIZE),+  log: joi.object().default(() => nullLogger),+  invalidation: joi.boolean().default(false)+})++/**+ * @typedef StorageMemoryOptions+ * @property {?number} [size=1024]+ * @property {?Logger} [log]+ * @property {?boolean} [invalidation=false]+ */++class StorageMemory extends StorageInterface {+  /**+   * in-memory storage+   * @param {StorageMemoryOptions} options+   */+  constructor (opts) {+    const { value: options, error } = optionsValidation.validate(opts || {})+    if (error) { throw error }++    super(options)+    this.size = options.size+    this.log = options.log+    this.invalidation = options.invalidation++    this.init()+  }++  init () {+    this.store = new LRUCache(this.size)++    if (!this.invalidation) {+      return+    }+    // key -> references, keys are strings, references are sorted array strings+    this.keysReferences = new Map()+    // same as above, but inverted+    this.referencesKeys = new Map()+  }++  /**+   * retrieve the value by key+   * @param {string} key+   * @returns {undefined|*} undefined if key not found or expired+   */+  async get (key) {+    this.log.debug({ msg: 'acd/storage/memory.get', key })++    const entry = this.store.get(key)+    if (entry) {+      this.log.debug({ msg: 'acd/storage/memory.get, entry', entry, now: now() })+      if (entry.start + entry.ttl > now()) {+        this.log.debug({ msg: 'acd/storage/memory.get, key is NOT expired', key, entry })+        return entry.value+      }+      this.log.debug({ msg: 'acd/storage/memory.get, key is EXPIRED', key, entry })++      // no need to wait for key to be removed++      setImmediate(() => this.remove(key))+    }+  }++  /**+   * set value by key+   * @param {string} key+   * @param {*} value+   * @param {?number} [ttl=0] - ttl in seconds; zero means key will not be stored+   * @param {?string[]} references+   */+  async set (key, value, ttl, references) {+    this.log.debug({ msg: 'acd/storage/memory.set', key, value, ttl, references })++    ttl = Number(ttl)+    if (!ttl || ttl < 0) {+      return+    }+    const existingKey = this.store.has(key)+    const removed = this.store.setpop(key, { value, ttl, start: now() })+    this.log.debug({ msg: 'acd/storage/memory.set, evicted', removed })+    if (removed && removed.evicted) {+      this.log.debug({ msg: 'acd/storage/memory.set, remove evicted key', key: removed.key })+      this._removeReferences([removed.key])+    }++    if (!references || references.length < 1) {+      return+    }++    if (!this.invalidation) {+      this.log.warn({ msg: 'acd/storage/memory.set, invalidation is disabled, references are useless' })+      return+    }++    // references must be unique+    references = [...new Set(references)]++    // clear old references+    let currentReferences+    if (existingKey) {+      currentReferences = this.keysReferences.get(key)+      this.log.debug({ msg: 'acd/storage/memory.set, current keys-references', key, references: currentReferences })+      if (currentReferences) {+        currentReferences.sort()+        references.sort()+        const referencesToRemove = findNotMatching(references, currentReferences)++        // remove key in old references+        for (const reference of referencesToRemove) {+          const keys = this.referencesKeys.get(reference)+          // istanbul ignore next+          if (!keys) { continue }+          const index = bsearchIndex(keys, key)+          // istanbul ignore next+          if (index < 0) { continue }+          keys.splice(index, 1)++          if (keys.length < 1) {+            this.referencesKeys.delete(reference)+            continue+          }+          this.referencesKeys.set(reference, keys)+        }+      }+    }++    // TODO we can probably get referencesToAdd and referencesToRemove in a single loop+    const referencesToAdd = currentReferences ? findNotMatching(currentReferences, references) : references++    for (let i = 0; i < referencesToAdd.length; i++) {+      const reference = referencesToAdd[i]+      let keys = this.referencesKeys.get(reference)+      if (keys) {+        this.log.debug({ msg: 'acd/storage/memory.set, add reference-key', key, reference })+        keys.push(key)+      } else {+        keys = [key]+      }+      this.log.debug({ msg: 'acd/storage/memory.set, set reference-keys', keys, reference })+      this.referencesKeys.set(reference, keys)+    }++    this.keysReferences.set(key, references)+  }++  /**+   * remove an entry by key+   * @param {string} key+   * @returns {boolean} indicates if key was removed+   */+  async remove (key) {+    this.log.debug({ msg: 'acd/storage/memory.remove', key })++    const removed = this._removeKey(key)+    this._removeReferences([key])+    return removed+  }++  /**+   * @param {string} key+   * @returns {boolean}+   */+  _removeKey (key) {+    this.log.debug({ msg: 'acd/storage/memory._removeKey', key })+    if (!this.store.has(key)) {+      return false+    }+    this.store.set(key, undefined)+    return true+  }++  /**+   * @param {string[]} keys+   */+  _removeReferences (keys) {+    if (!this.invalidation) {+      return+    }+    this.log.debug({ msg: 'acd/storage/memory._removeReferences', keys })++    const referencesToRemove = new Set()+    for (let i = 0; i < keys.length; i++) {+      const key = keys[i]++      const references = this.keysReferences.get(key)+      if (!references) {+        continue+      }++      for (let j = 0; j < references.length; j++) {+        referencesToRemove.add(references[j])+      }++      this.log.debug({ msg: 'acd/storage/memory._removeReferences, delete key-references', key })+      this.keysReferences.delete(key)+    }++    this._removeReferencesKeys([...referencesToRemove], keys)+  }++  /**+   * @param {!string[]} references+   * @param {string[]} keys+   */+  _removeReferencesKeys (references, keys) {+    keys.sort()+    this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys', references, keys })+    for (let i = 0; i < references.length; i++) {+      const reference = references[i]+      // working on the original stored array+      const referencesKeys = this.referencesKeys.get(reference)+      this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, get reference-key', reference, keys, referencesKeys })+      // istanbul ignore next+      if (!referencesKeys) continue++      const referencesToRemove = findMatchingIndexes(keys, referencesKeys)+      // cannot happen that referencesToRemove is empty+      // because this function is triggered only by _removeReferences+      // and "keys" are from tis.keyReferences+      // if (referencesToRemove.length < 1) { continue }++      this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, removing', reference, referencesToRemove, referencesKeys })++      if (referencesToRemove.length === referencesKeys.length) {+        this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, delete', reference })+        this.referencesKeys.delete(reference)+        continue+      }++      for (let j = referencesToRemove.length - 1; j >= 0; j--) {+        this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, remove', reference, referencesKeys, at: referencesToRemove[j] })+        referencesKeys.splice(referencesToRemove[j], 1)+      }+    }+  }++  /**+   * @param {string[]} references+   * @returns {string[]} removed keys+   */+  async invalidate (references) {+    if (!this.invalidation) {+      this.log.warn({ msg: 'acd/storage/memory.invalidate, exit due invalidation is disabled' })+      return []+    }++    this.log.debug({ msg: 'acd/storage/memory.invalidate', references })++    const removed = []+    for (let i = 0; i < references.length; i++) {+      const reference = references[i]+      const keys = this.referencesKeys.get(reference)+      this.log.debug({ msg: 'acd/storage/memory.invalidate, remove keys on reference', reference, keys })+      if (!keys) {+        continue+      }++      for (let j = 0; j < keys.length; j++) {+        const key = keys[j]+        this.log.debug({ msg: 'acd/storage/memory.invalidate, remove key on reference', reference, key })+        // istanbul ignore next+        if (this._removeKey(key)) {+          removed.push(key)+        }+      }++      this.log.debug({ msg: 'acd/storage/memory.invalidate, remove references of', reference, keys })+      this._removeReferences([...keys])+    }++    return removed+  }++  /**+   * remove all entries if name is not provided+   * remove entries where key starts with name if provided+   * @param {?string} name+   * @return {string[]} removed keys+   */+  async clear (name) {
 clear (name) {
simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')+const LRUCache = require('mnemonist/lru-cache')+const nullLogger = require('abstract-logging')+const StorageInterface = require('./interface')+const { findMatchingIndexes, findNotMatching, bsearchIndex } = require('../util')++const DEFAULT_CACHE_SIZE = 1024++const optionsValidation = joi.object({+  size: joi.number().integer().min(1).default(DEFAULT_CACHE_SIZE),+  log: joi.object().default(() => nullLogger),+  invalidation: joi.boolean().default(false)+})++/**+ * @typedef StorageMemoryOptions+ * @property {?number} [size=1024]+ * @property {?Logger} [log]+ * @property {?boolean} [invalidation=false]+ */++class StorageMemory extends StorageInterface {+  /**+   * in-memory storage+   * @param {StorageMemoryOptions} options+   */+  constructor (opts) {+    const { value: options, error } = optionsValidation.validate(opts || {})+    if (error) { throw error }++    super(options)+    this.size = options.size+    this.log = options.log+    this.invalidation = options.invalidation++    this.init()+  }++  init () {+    this.store = new LRUCache(this.size)++    if (!this.invalidation) {+      return+    }+    // key -> references, keys are strings, references are sorted array strings+    this.keysReferences = new Map()+    // same as above, but inverted+    this.referencesKeys = new Map()+  }++  /**+   * retrieve the value by key+   * @param {string} key+   * @returns {undefined|*} undefined if key not found or expired+   */+  async get (key) {+    this.log.debug({ msg: 'acd/storage/memory.get', key })++    const entry = this.store.get(key)+    if (entry) {+      this.log.debug({ msg: 'acd/storage/memory.get, entry', entry, now: now() })+      if (entry.start + entry.ttl > now()) {+        this.log.debug({ msg: 'acd/storage/memory.get, key is NOT expired', key, entry })+        return entry.value+      }+      this.log.debug({ msg: 'acd/storage/memory.get, key is EXPIRED', key, entry })++      // no need to wait for key to be removed++      setImmediate(() => this.remove(key))+    }+  }++  /**+   * set value by key+   * @param {string} key+   * @param {*} value+   * @param {?number} [ttl=0] - ttl in seconds; zero means key will not be stored+   * @param {?string[]} references+   */+  async set (key, value, ttl, references) {+    this.log.debug({ msg: 'acd/storage/memory.set', key, value, ttl, references })++    ttl = Number(ttl)+    if (!ttl || ttl < 0) {+      return+    }+    const existingKey = this.store.has(key)+    const removed = this.store.setpop(key, { value, ttl, start: now() })+    this.log.debug({ msg: 'acd/storage/memory.set, evicted', removed })+    if (removed && removed.evicted) {+      this.log.debug({ msg: 'acd/storage/memory.set, remove evicted key', key: removed.key })+      this._removeReferences([removed.key])+    }++    if (!references || references.length < 1) {+      return+    }++    if (!this.invalidation) {+      this.log.warn({ msg: 'acd/storage/memory.set, invalidation is disabled, references are useless' })+      return+    }++    // references must be unique+    references = [...new Set(references)]++    // clear old references+    let currentReferences+    if (existingKey) {+      currentReferences = this.keysReferences.get(key)+      this.log.debug({ msg: 'acd/storage/memory.set, current keys-references', key, references: currentReferences })+      if (currentReferences) {+        currentReferences.sort()+        references.sort()+        const referencesToRemove = findNotMatching(references, currentReferences)++        // remove key in old references+        for (const reference of referencesToRemove) {+          const keys = this.referencesKeys.get(reference)+          // istanbul ignore next+          if (!keys) { continue }+          const index = bsearchIndex(keys, key)+          // istanbul ignore next+          if (index < 0) { continue }+          keys.splice(index, 1)++          if (keys.length < 1) {+            this.referencesKeys.delete(reference)+            continue+          }+          this.referencesKeys.set(reference, keys)+        }+      }+    }++    // TODO we can probably get referencesToAdd and referencesToRemove in a single loop+    const referencesToAdd = currentReferences ? findNotMatching(currentReferences, references) : references++    for (let i = 0; i < referencesToAdd.length; i++) {+      const reference = referencesToAdd[i]+      let keys = this.referencesKeys.get(reference)+      if (keys) {+        this.log.debug({ msg: 'acd/storage/memory.set, add reference-key', key, reference })+        keys.push(key)+      } else {+        keys = [key]+      }+      this.log.debug({ msg: 'acd/storage/memory.set, set reference-keys', keys, reference })+      this.referencesKeys.set(reference, keys)+    }++    this.keysReferences.set(key, references)+  }++  /**+   * remove an entry by key+   * @param {string} key+   * @returns {boolean} indicates if key was removed+   */+  async remove (key) {+    this.log.debug({ msg: 'acd/storage/memory.remove', key })++    const removed = this._removeKey(key)+    this._removeReferences([key])+    return removed+  }++  /**+   * @param {string} key+   * @returns {boolean}+   */+  _removeKey (key) {+    this.log.debug({ msg: 'acd/storage/memory._removeKey', key })+    if (!this.store.has(key)) {+      return false+    }+    this.store.set(key, undefined)+    return true+  }++  /**+   * @param {string[]} keys+   */+  _removeReferences (keys) {+    if (!this.invalidation) {+      return+    }+    this.log.debug({ msg: 'acd/storage/memory._removeReferences', keys })++    const referencesToRemove = new Set()+    for (let i = 0; i < keys.length; i++) {+      const key = keys[i]++      const references = this.keysReferences.get(key)+      if (!references) {+        continue+      }++      for (let j = 0; j < references.length; j++) {+        referencesToRemove.add(references[j])+      }++      this.log.debug({ msg: 'acd/storage/memory._removeReferences, delete key-references', key })+      this.keysReferences.delete(key)+    }++    this._removeReferencesKeys([...referencesToRemove], keys)+  }++  /**+   * @param {!string[]} references+   * @param {string[]} keys+   */+  _removeReferencesKeys (references, keys) {+    keys.sort()+    this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys', references, keys })+    for (let i = 0; i < references.length; i++) {+      const reference = references[i]+      // working on the original stored array+      const referencesKeys = this.referencesKeys.get(reference)+      this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, get reference-key', reference, keys, referencesKeys })+      // istanbul ignore next+      if (!referencesKeys) continue++      const referencesToRemove = findMatchingIndexes(keys, referencesKeys)+      // cannot happen that referencesToRemove is empty+      // because this function is triggered only by _removeReferences+      // and "keys" are from tis.keyReferences+      // if (referencesToRemove.length < 1) { continue }++      this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, removing', reference, referencesToRemove, referencesKeys })++      if (referencesToRemove.length === referencesKeys.length) {+        this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, delete', reference })+        this.referencesKeys.delete(reference)+        continue+      }++      for (let j = referencesToRemove.length - 1; j >= 0; j--) {+        this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, remove', reference, referencesKeys, at: referencesToRemove[j] })+        referencesKeys.splice(referencesToRemove[j], 1)+      }+    }+  }++  /**+   * @param {string[]} references+   * @returns {string[]} removed keys+   */+  async invalidate (references) {+    if (!this.invalidation) {+      this.log.warn({ msg: 'acd/storage/memory.invalidate, exit due invalidation is disabled' })+      return []+    }++    this.log.debug({ msg: 'acd/storage/memory.invalidate', references })++    const removed = []+    for (let i = 0; i < references.length; i++) {+      const reference = references[i]+      const keys = this.referencesKeys.get(reference)+      this.log.debug({ msg: 'acd/storage/memory.invalidate, remove keys on reference', reference, keys })+      if (!keys) {+        continue+      }++      for (let j = 0; j < keys.length; j++) {+        const key = keys[j]+        this.log.debug({ msg: 'acd/storage/memory.invalidate, remove key on reference', reference, key })+        // istanbul ignore next+        if (this._removeKey(key)) {+          removed.push(key)+        }+      }++      this.log.debug({ msg: 'acd/storage/memory.invalidate, remove references of', reference, keys })+      this._removeReferences([...keys])+    }++    return removed+  }++  /**+   * remove all entries if name is not provided+   * remove entries where key starts with name if provided+   * @param {?string} name+   * @return {string[]} removed keys+   */+  async clear (name) {+    this.log.debug({ msg: 'acd/storage/memory.clear', name })++    if (!name) {+      this.store.clear()+      if (!this.invalidation) { return }+      this.referencesKeys.clear()+      this.keysReferences.clear()+      return+    }++    const keys = []+    this.store.forEach((value, key) => {+      this.log.debug({ msg: 'acd/storage/memory.clear, iterate key', key })+      if (key.indexOf(name) === 0) {+        this.log.debug({ msg: 'acd/storage/memory.clear, remove key', key })+        // can't remove here or the loop won't work+        keys.push(key)+      }+    })++    const removed = []+    // remove all keys at first, then references+    for (let i = 0; i < keys.length; i++) {+      // istanbul ignore next+      if (this._removeKey(keys[i])) {+        removed.push(keys[i])+      }+    }++    this._removeReferences(removed)++    return removed+  }++  async refresh () {
 refresh () {
simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')+const LRUCache = require('mnemonist/lru-cache')+const nullLogger = require('abstract-logging')+const StorageInterface = require('./interface')+const { findMatchingIndexes, findNotMatching, bsearchIndex } = require('../util')++const DEFAULT_CACHE_SIZE = 1024++const optionsValidation = joi.object({+  size: joi.number().integer().min(1).default(DEFAULT_CACHE_SIZE),+  log: joi.object().default(() => nullLogger),+  invalidation: joi.boolean().default(false)+})++/**+ * @typedef StorageMemoryOptions+ * @property {?number} [size=1024]+ * @property {?Logger} [log]+ * @property {?boolean} [invalidation=false]+ */++class StorageMemory extends StorageInterface {+  /**+   * in-memory storage+   * @param {StorageMemoryOptions} options+   */+  constructor (opts) {+    const { value: options, error } = optionsValidation.validate(opts || {})+    if (error) { throw error }++    super(options)+    this.size = options.size+    this.log = options.log+    this.invalidation = options.invalidation++    this.init()+  }++  init () {+    this.store = new LRUCache(this.size)++    if (!this.invalidation) {+      return+    }+    // key -> references, keys are strings, references are sorted array strings+    this.keysReferences = new Map()+    // same as above, but inverted+    this.referencesKeys = new Map()+  }++  /**+   * retrieve the value by key+   * @param {string} key+   * @returns {undefined|*} undefined if key not found or expired+   */+  async get (key) {+    this.log.debug({ msg: 'acd/storage/memory.get', key })++    const entry = this.store.get(key)+    if (entry) {+      this.log.debug({ msg: 'acd/storage/memory.get, entry', entry, now: now() })+      if (entry.start + entry.ttl > now()) {+        this.log.debug({ msg: 'acd/storage/memory.get, key is NOT expired', key, entry })+        return entry.value+      }+      this.log.debug({ msg: 'acd/storage/memory.get, key is EXPIRED', key, entry })++      // no need to wait for key to be removed++      setImmediate(() => this.remove(key))+    }+  }++  /**+   * set value by key+   * @param {string} key+   * @param {*} value+   * @param {?number} [ttl=0] - ttl in seconds; zero means key will not be stored+   * @param {?string[]} references+   */+  async set (key, value, ttl, references) {+    this.log.debug({ msg: 'acd/storage/memory.set', key, value, ttl, references })++    ttl = Number(ttl)+    if (!ttl || ttl < 0) {+      return+    }+    const existingKey = this.store.has(key)+    const removed = this.store.setpop(key, { value, ttl, start: now() })+    this.log.debug({ msg: 'acd/storage/memory.set, evicted', removed })+    if (removed && removed.evicted) {+      this.log.debug({ msg: 'acd/storage/memory.set, remove evicted key', key: removed.key })+      this._removeReferences([removed.key])+    }++    if (!references || references.length < 1) {+      return+    }++    if (!this.invalidation) {+      this.log.warn({ msg: 'acd/storage/memory.set, invalidation is disabled, references are useless' })+      return+    }++    // references must be unique+    references = [...new Set(references)]++    // clear old references+    let currentReferences+    if (existingKey) {+      currentReferences = this.keysReferences.get(key)+      this.log.debug({ msg: 'acd/storage/memory.set, current keys-references', key, references: currentReferences })+      if (currentReferences) {+        currentReferences.sort()+        references.sort()+        const referencesToRemove = findNotMatching(references, currentReferences)++        // remove key in old references+        for (const reference of referencesToRemove) {+          const keys = this.referencesKeys.get(reference)+          // istanbul ignore next+          if (!keys) { continue }+          const index = bsearchIndex(keys, key)+          // istanbul ignore next+          if (index < 0) { continue }+          keys.splice(index, 1)++          if (keys.length < 1) {+            this.referencesKeys.delete(reference)+            continue+          }+          this.referencesKeys.set(reference, keys)+        }+      }+    }++    // TODO we can probably get referencesToAdd and referencesToRemove in a single loop+    const referencesToAdd = currentReferences ? findNotMatching(currentReferences, references) : references++    for (let i = 0; i < referencesToAdd.length; i++) {+      const reference = referencesToAdd[i]+      let keys = this.referencesKeys.get(reference)+      if (keys) {+        this.log.debug({ msg: 'acd/storage/memory.set, add reference-key', key, reference })+        keys.push(key)+      } else {+        keys = [key]+      }+      this.log.debug({ msg: 'acd/storage/memory.set, set reference-keys', keys, reference })+      this.referencesKeys.set(reference, keys)+    }++    this.keysReferences.set(key, references)+  }++  /**+   * remove an entry by key+   * @param {string} key+   * @returns {boolean} indicates if key was removed+   */+  async remove (key) {+    this.log.debug({ msg: 'acd/storage/memory.remove', key })++    const removed = this._removeKey(key)+    this._removeReferences([key])+    return removed+  }++  /**+   * @param {string} key+   * @returns {boolean}+   */+  _removeKey (key) {+    this.log.debug({ msg: 'acd/storage/memory._removeKey', key })+    if (!this.store.has(key)) {+      return false+    }+    this.store.set(key, undefined)+    return true+  }++  /**+   * @param {string[]} keys+   */+  _removeReferences (keys) {+    if (!this.invalidation) {+      return+    }+    this.log.debug({ msg: 'acd/storage/memory._removeReferences', keys })++    const referencesToRemove = new Set()+    for (let i = 0; i < keys.length; i++) {+      const key = keys[i]++      const references = this.keysReferences.get(key)+      if (!references) {+        continue+      }++      for (let j = 0; j < references.length; j++) {+        referencesToRemove.add(references[j])+      }++      this.log.debug({ msg: 'acd/storage/memory._removeReferences, delete key-references', key })+      this.keysReferences.delete(key)+    }++    this._removeReferencesKeys([...referencesToRemove], keys)+  }++  /**+   * @param {!string[]} references+   * @param {string[]} keys+   */+  _removeReferencesKeys (references, keys) {+    keys.sort()+    this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys', references, keys })+    for (let i = 0; i < references.length; i++) {+      const reference = references[i]+      // working on the original stored array+      const referencesKeys = this.referencesKeys.get(reference)+      this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, get reference-key', reference, keys, referencesKeys })+      // istanbul ignore next+      if (!referencesKeys) continue++      const referencesToRemove = findMatchingIndexes(keys, referencesKeys)+      // cannot happen that referencesToRemove is empty+      // because this function is triggered only by _removeReferences+      // and "keys" are from tis.keyReferences+      // if (referencesToRemove.length < 1) { continue }++      this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, removing', reference, referencesToRemove, referencesKeys })++      if (referencesToRemove.length === referencesKeys.length) {+        this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, delete', reference })+        this.referencesKeys.delete(reference)+        continue+      }++      for (let j = referencesToRemove.length - 1; j >= 0; j--) {+        this.log.debug({ msg: 'acd/storage/memory._removeReferencesKeys, remove', reference, referencesKeys, at: referencesToRemove[j] })+        referencesKeys.splice(referencesToRemove[j], 1)+      }+    }+  }++  /**+   * @param {string[]} references+   * @returns {string[]} removed keys+   */+  async invalidate (references) {
 invalidate (references) {
simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')+const LRUCache = require('mnemonist/lru-cache')+const nullLogger = require('abstract-logging')+const StorageInterface = require('./interface')+const { findMatchingIndexes, findNotMatching, bsearchIndex } = require('../util')++const DEFAULT_CACHE_SIZE = 1024++const optionsValidation = joi.object({+  size: joi.number().integer().min(1).default(DEFAULT_CACHE_SIZE),+  log: joi.object().default(() => nullLogger),+  invalidation: joi.boolean().default(false)+})++/**+ * @typedef StorageMemoryOptions+ * @property {?number} [size=1024]+ * @property {?Logger} [log]+ * @property {?boolean} [invalidation=false]+ */++class StorageMemory extends StorageInterface {+  /**+   * in-memory storage+   * @param {StorageMemoryOptions} options+   */+  constructor (opts) {+    const { value: options, error } = optionsValidation.validate(opts || {})+    if (error) { throw error }++    super(options)+    this.size = options.size+    this.log = options.log+    this.invalidation = options.invalidation++    this.init()+  }++  init () {+    this.store = new LRUCache(this.size)++    if (!this.invalidation) {+      return+    }+    // key -> references, keys are strings, references are sorted array strings+    this.keysReferences = new Map()+    // same as above, but inverted+    this.referencesKeys = new Map()+  }++  /**+   * retrieve the value by key+   * @param {string} key+   * @returns {undefined|*} undefined if key not found or expired+   */+  async get (key) {+    this.log.debug({ msg: 'acd/storage/memory.get', key })++    const entry = this.store.get(key)+    if (entry) {+      this.log.debug({ msg: 'acd/storage/memory.get, entry', entry, now: now() })+      if (entry.start + entry.ttl > now()) {+        this.log.debug({ msg: 'acd/storage/memory.get, key is NOT expired', key, entry })+        return entry.value+      }+      this.log.debug({ msg: 'acd/storage/memory.get, key is EXPIRED', key, entry })++      // no need to wait for key to be removed++      setImmediate(() => this.remove(key))+    }+  }++  /**+   * set value by key+   * @param {string} key+   * @param {*} value+   * @param {?number} [ttl=0] - ttl in seconds; zero means key will not be stored+   * @param {?string[]} references+   */+  async set (key, value, ttl, references) {+    this.log.debug({ msg: 'acd/storage/memory.set', key, value, ttl, references })++    ttl = Number(ttl)+    if (!ttl || ttl < 0) {+      return+    }+    const existingKey = this.store.has(key)+    const removed = this.store.setpop(key, { value, ttl, start: now() })+    this.log.debug({ msg: 'acd/storage/memory.set, evicted', removed })+    if (removed && removed.evicted) {+      this.log.debug({ msg: 'acd/storage/memory.set, remove evicted key', key: removed.key })+      this._removeReferences([removed.key])+    }++    if (!references || references.length < 1) {+      return+    }++    if (!this.invalidation) {+      this.log.warn({ msg: 'acd/storage/memory.set, invalidation is disabled, references are useless' })+      return+    }++    // references must be unique+    references = [...new Set(references)]++    // clear old references+    let currentReferences+    if (existingKey) {+      currentReferences = this.keysReferences.get(key)+      this.log.debug({ msg: 'acd/storage/memory.set, current keys-references', key, references: currentReferences })+      if (currentReferences) {+        currentReferences.sort()+        references.sort()+        const referencesToRemove = findNotMatching(references, currentReferences)++        // remove key in old references+        for (const reference of referencesToRemove) {+          const keys = this.referencesKeys.get(reference)+          // istanbul ignore next+          if (!keys) { continue }+          const index = bsearchIndex(keys, key)+          // istanbul ignore next+          if (index < 0) { continue }+          keys.splice(index, 1)++          if (keys.length < 1) {+            this.referencesKeys.delete(reference)+            continue+          }+          this.referencesKeys.set(reference, keys)+        }+      }+    }++    // TODO we can probably get referencesToAdd and referencesToRemove in a single loop+    const referencesToAdd = currentReferences ? findNotMatching(currentReferences, references) : references++    for (let i = 0; i < referencesToAdd.length; i++) {+      const reference = referencesToAdd[i]+      let keys = this.referencesKeys.get(reference)+      if (keys) {+        this.log.debug({ msg: 'acd/storage/memory.set, add reference-key', key, reference })+        keys.push(key)+      } else {+        keys = [key]+      }+      this.log.debug({ msg: 'acd/storage/memory.set, set reference-keys', keys, reference })+      this.referencesKeys.set(reference, keys)+    }++    this.keysReferences.set(key, references)+  }++  /**+   * remove an entry by key+   * @param {string} key+   * @returns {boolean} indicates if key was removed+   */+  async remove (key) {
 remove (key) {
simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')+const LRUCache = require('mnemonist/lru-cache')+const nullLogger = require('abstract-logging')+const StorageInterface = require('./interface')+const { findMatchingIndexes, findNotMatching, bsearchIndex } = require('../util')++const DEFAULT_CACHE_SIZE = 1024++const optionsValidation = joi.object({+  size: joi.number().integer().min(1).default(DEFAULT_CACHE_SIZE),+  log: joi.object().default(() => nullLogger),+  invalidation: joi.boolean().default(false)+})++/**+ * @typedef StorageMemoryOptions+ * @property {?number} [size=1024]+ * @property {?Logger} [log]+ * @property {?boolean} [invalidation=false]+ */++class StorageMemory extends StorageInterface {+  /**+   * in-memory storage+   * @param {StorageMemoryOptions} options+   */+  constructor (opts) {+    const { value: options, error } = optionsValidation.validate(opts || {})+    if (error) { throw error }++    super(options)+    this.size = options.size+    this.log = options.log+    this.invalidation = options.invalidation++    this.init()+  }++  init () {+    this.store = new LRUCache(this.size)++    if (!this.invalidation) {+      return+    }+    // key -> references, keys are strings, references are sorted array strings+    this.keysReferences = new Map()+    // same as above, but inverted+    this.referencesKeys = new Map()+  }++  /**+   * retrieve the value by key+   * @param {string} key+   * @returns {undefined|*} undefined if key not found or expired+   */+  async get (key) {
 get (key) {
simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

 'use strict' -const kValues = require('./symbol')+const { kValues, kStorage, kTTL, kOnDedupe, kOnHit, kOnMiss } = require('./symbol') const stringify = require('safe-stable-stringify')-const LRUCache = require('mnemonist/lru-cache')+const joi = require('joi') -const kCacheSize = Symbol('kCacheSize')-const kTTL = Symbol('kTTL')-const kOnHit = Symbol('kOnHit')+const cacheOptionsValidation = joi.object({+  storage: joi.object().required(),+  ttl: joi.number().integer().min(0).default(0),+  onDedupe: joi.func().default(() => noop),+  onHit: joi.func().default(() => noop),+  onMiss: joi.func().default(() => noop)+})  class Cache {+  /**+   * @param {!Object} opts+   * @param {!Storage} opts.storage - the storage 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.onHit+   * @param {?function} opts.onMiss+   */   constructor (opts) {-    opts = opts || {}+    const { value: options, error } = cacheOptionsValidation.validate(opts || {})+    if (error) { throw error }+     this[kValues] = {}-    this[kCacheSize] = opts.cacheSize || 1024-    this[kTTL] = opts.ttl || 0-    this[kOnHit] = opts.onHit || noop+    this[kStorage] = options.storage+    this[kTTL] = options.ttl+    this[kOnDedupe] = options.onDedupe+    this[kOnHit] = options.onHit+    this[kOnMiss] = options.onMiss   } -  define (key, opts, func) {+  /**+   * add a new function to dedupe (and cache)+   * @param {!string} name name of the function+   * @param {?Object} [opts]+   * @param {?function} [opts.storage] storage to use; default is the one passed to the constructor+   * @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.onHit] function to call on hit; default is the one passed to the constructor+   * @param {?function} [opts.onMiss] function to call on miss; default is the one passed to the constructor+   * @param {?function} [opts.serialize] custom function to serialize the arguments of `func`, in order to create the key for deduping and caching+   * @param {?function} [opts.references] function to generate references+   * @param {!function} func the function to dedupe (and cache)+   **/+  define (name, opts, func) {     if (typeof opts === 'function') {       func = opts       opts = {}     } -    if (key && this[key]) {-      throw new Error(`${key} is already defined in the cache or it is a forbidden name`)+    if (name && this[name]) {+      throw new Error(`${name} is already defined in the cache or it is a forbidden name`)     }      opts = opts || {}      if (typeof func !== 'function') {-      throw new TypeError(`Missing the function parameter for '${key}'`)+      throw new TypeError(`Missing the function parameter for '${name}'`)     }      const serialize = opts.serialize     if (serialize && typeof serialize !== 'function') {       throw new TypeError('serialize must be a function')     } -    const cacheSize = opts.cacheSize || this[kCacheSize]+    const references = opts.references+    if (references && typeof references !== 'function') {+      throw new TypeError('references must be a function')+    }++    // TODO doc we could even have a different storage for each key+    const storage = opts.storage || this[kStorage]     const ttl = opts.ttl || this[kTTL]+    const onDedupe = opts.onDedupe || this[kOnDedupe]     const onHit = opts.onHit || this[kOnHit]+    const onMiss = opts.onMiss || this[kOnMiss] -    const wrapper = new Wrapper(func, key, serialize, cacheSize, ttl, onHit)+    const wrapper = new Wrapper(func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) -    this[kValues][key] = wrapper-    this[key] = wrapper.add.bind(wrapper)+    this[kValues][name] = wrapper+    this[name] = wrapper.add.bind(wrapper)   } -  clear (key, value) {-    if (key) {-      this[kValues][key].clear(value)+  async clear (name, value) {+    if (name) {+      if (!this[kValues][name]) {+        throw new Error(`${name} is not defined in the cache`)+      }++      await this[kValues][name].clear(value)       return     } +    const clears = []     for (const wrapper of Object.values(this[kValues])) {-      wrapper.clear()+      clears.push(wrapper.clear())     }+    await Promise.all(clears)   }-} -let _currentSecond+  async get (name, key) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key? -function currentSecond () {-  if (_currentSecond !== undefined) {-    return _currentSecond+    return this[kValues][name].get(key)   }-  _currentSecond = Math.floor(Date.now() / 1000)-  setTimeout(_clearSecond, 1000).unref()-  return _currentSecond-} -function _clearSecond () {-  _currentSecond = undefined+  async set (name, key, value, ttl, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key, value, ttl, references?++    return this[kValues][name].set(key, value, ttl, references)+  }++  async invalidate (name, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate references?++    return this[kValues][name].invalidate(references)+  } }  class Wrapper {-  constructor (func, key, serialize, cacheSize, ttl, onHit) {-    this.ids = new LRUCache(cacheSize)-    this.error = null-    this.started = false+  // TODO signature+  constructor (func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) {+    this.dedupes = new Map()     this.func = func-    this.key = key+    this.name = name     this.serialize = serialize+    this.references = references++    this.storage = storage     this.ttl = ttl+    this.onDedupe = onDedupe     this.onHit = onHit-  }--  buildPromise (query, args, key) {-    query.promise = this.func(args, key)-    // we fork the promise chain on purpose-    const p = query.promise.catch(() => this.ids.set(key, undefined))-    if (this.ttl > 0) {-      query.cachedOn = currentSecond()-    } else {-      // clear the cache if there is no TTL-      p.then(() => this.ids.set(key, undefined))-    }+    this.onMiss = onMiss   }    getKey (args) {     const id = this.serialize ? this.serialize(args) : args     return typeof id === 'string' ? id : stringify(id)   } +  getStorageKey (key) {+    return `${this.name}~${key}`+  }++  getStorageName () {+    return `${this.name}~`+  }+   add (args) {     const key = this.getKey(args)-    const onHit = this.onHit -    let query = this.ids.get(key)+    let query = this.dedupes.get(key)     if (!query) {       query = new Query()       this.buildPromise(query, args, key)-      this.ids.set(key, query)-    } else if (this.ttl > 0) {-      onHit()-      if (currentSecond() - query.cachedOn > this.ttl) {-        // restart-        this.buildPromise(query, args, key)-      }+      this.dedupes.set(key, query)     } else {-      onHit()+      this.onDedupe(key)     }      return query.promise   } -  clear (value) {+  /**+   * wrap the original func to sync storage+   */+  async wrapFunction (args, key) {+    const storageKey = this.getStorageKey(key)+    const data = await this.storage.get(storageKey)+    if (data !== undefined) {+      this.onHit(key)+      return data+    }++    this.onMiss(key)++    const result = await this.func(args, key)++    if (this.ttl < 1) {+      return result+    }++    if (!this.references) {+      await this.storage.set(storageKey, result, this.ttl)+      return result+    }++    const references = await this.references(args, key, result)+    // TODO validate references?

still TODO?

simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

 'use strict' -const kValues = require('./symbol')+const { kValues, kStorage, kTTL, kOnDedupe, kOnHit, kOnMiss } = require('./symbol') const stringify = require('safe-stable-stringify')-const LRUCache = require('mnemonist/lru-cache')+const joi = require('joi') -const kCacheSize = Symbol('kCacheSize')-const kTTL = Symbol('kTTL')-const kOnHit = Symbol('kOnHit')+const cacheOptionsValidation = joi.object({+  storage: joi.object().required(),+  ttl: joi.number().integer().min(0).default(0),+  onDedupe: joi.func().default(() => noop),+  onHit: joi.func().default(() => noop),+  onMiss: joi.func().default(() => noop)+})  class Cache {+  /**+   * @param {!Object} opts+   * @param {!Storage} opts.storage - the storage 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.onHit+   * @param {?function} opts.onMiss+   */   constructor (opts) {-    opts = opts || {}+    const { value: options, error } = cacheOptionsValidation.validate(opts || {})+    if (error) { throw error }+     this[kValues] = {}-    this[kCacheSize] = opts.cacheSize || 1024-    this[kTTL] = opts.ttl || 0-    this[kOnHit] = opts.onHit || noop+    this[kStorage] = options.storage+    this[kTTL] = options.ttl+    this[kOnDedupe] = options.onDedupe+    this[kOnHit] = options.onHit+    this[kOnMiss] = options.onMiss   } -  define (key, opts, func) {+  /**+   * add a new function to dedupe (and cache)+   * @param {!string} name name of the function+   * @param {?Object} [opts]+   * @param {?function} [opts.storage] storage to use; default is the one passed to the constructor+   * @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.onHit] function to call on hit; default is the one passed to the constructor+   * @param {?function} [opts.onMiss] function to call on miss; default is the one passed to the constructor+   * @param {?function} [opts.serialize] custom function to serialize the arguments of `func`, in order to create the key for deduping and caching+   * @param {?function} [opts.references] function to generate references+   * @param {!function} func the function to dedupe (and cache)+   **/+  define (name, opts, func) {     if (typeof opts === 'function') {       func = opts       opts = {}     } -    if (key && this[key]) {-      throw new Error(`${key} is already defined in the cache or it is a forbidden name`)+    if (name && this[name]) {+      throw new Error(`${name} is already defined in the cache or it is a forbidden name`)     }      opts = opts || {}      if (typeof func !== 'function') {-      throw new TypeError(`Missing the function parameter for '${key}'`)+      throw new TypeError(`Missing the function parameter for '${name}'`)     }      const serialize = opts.serialize     if (serialize && typeof serialize !== 'function') {       throw new TypeError('serialize must be a function')     } -    const cacheSize = opts.cacheSize || this[kCacheSize]+    const references = opts.references+    if (references && typeof references !== 'function') {+      throw new TypeError('references must be a function')+    }++    // TODO doc we could even have a different storage for each key+    const storage = opts.storage || this[kStorage]     const ttl = opts.ttl || this[kTTL]+    const onDedupe = opts.onDedupe || this[kOnDedupe]     const onHit = opts.onHit || this[kOnHit]+    const onMiss = opts.onMiss || this[kOnMiss] -    const wrapper = new Wrapper(func, key, serialize, cacheSize, ttl, onHit)+    const wrapper = new Wrapper(func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) -    this[kValues][key] = wrapper-    this[key] = wrapper.add.bind(wrapper)+    this[kValues][name] = wrapper+    this[name] = wrapper.add.bind(wrapper)   } -  clear (key, value) {-    if (key) {-      this[kValues][key].clear(value)+  async clear (name, value) {+    if (name) {+      if (!this[kValues][name]) {+        throw new Error(`${name} is not defined in the cache`)+      }++      await this[kValues][name].clear(value)       return     } +    const clears = []     for (const wrapper of Object.values(this[kValues])) {-      wrapper.clear()+      clears.push(wrapper.clear())     }+    await Promise.all(clears)   }-} -let _currentSecond+  async get (name, key) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key? -function currentSecond () {-  if (_currentSecond !== undefined) {-    return _currentSecond+    return this[kValues][name].get(key)   }-  _currentSecond = Math.floor(Date.now() / 1000)-  setTimeout(_clearSecond, 1000).unref()-  return _currentSecond-} -function _clearSecond () {-  _currentSecond = undefined+  async set (name, key, value, ttl, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key, value, ttl, references?++    return this[kValues][name].set(key, value, ttl, references)+  }++  async invalidate (name, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate references?++    return this[kValues][name].invalidate(references)+  } }  class Wrapper {-  constructor (func, key, serialize, cacheSize, ttl, onHit) {-    this.ids = new LRUCache(cacheSize)-    this.error = null-    this.started = false+  // TODO signature+  constructor (func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) {+    this.dedupes = new Map()     this.func = func-    this.key = key+    this.name = name     this.serialize = serialize+    this.references = references++    this.storage = storage     this.ttl = ttl+    this.onDedupe = onDedupe     this.onHit = onHit-  }--  buildPromise (query, args, key) {-    query.promise = this.func(args, key)-    // we fork the promise chain on purpose-    const p = query.promise.catch(() => this.ids.set(key, undefined))-    if (this.ttl > 0) {-      query.cachedOn = currentSecond()-    } else {-      // clear the cache if there is no TTL-      p.then(() => this.ids.set(key, undefined))-    }+    this.onMiss = onMiss   }    getKey (args) {     const id = this.serialize ? this.serialize(args) : args     return typeof id === 'string' ? id : stringify(id)   } +  getStorageKey (key) {+    return `${this.name}~${key}`+  }++  getStorageName () {+    return `${this.name}~`+  }+   add (args) {     const key = this.getKey(args)-    const onHit = this.onHit -    let query = this.ids.get(key)+    let query = this.dedupes.get(key)     if (!query) {       query = new Query()       this.buildPromise(query, args, key)-      this.ids.set(key, query)-    } else if (this.ttl > 0) {-      onHit()-      if (currentSecond() - query.cachedOn > this.ttl) {-        // restart-        this.buildPromise(query, args, key)-      }+      this.dedupes.set(key, query)     } else {-      onHit()+      this.onDedupe(key)     }      return query.promise   } -  clear (value) {+  /**+   * wrap the original func to sync storage+   */+  async wrapFunction (args, key) {+    const storageKey = this.getStorageKey(key)+    const data = await this.storage.get(storageKey)

Avoid the await if the returned value is not a promise.

simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const createStorage = require('../storage')+const { Cache } = require('../')+const Redis = require('ioredis')++// TODO++async function main () {+  const redisClient = new Redis()+  const redisListener = new Redis()++  const cache = new Cache({+    ttl: 2, // default ttl, in seconds+    storage: createStorage('redis', { client: redisClient, log: console }),+    listener: redisListener,

I don't think we use this.

simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

 'use strict' -const kValues = require('./symbol')+const { kValues, kStorage, kTTL, kOnDedupe, kOnHit, kOnMiss } = require('./symbol') const stringify = require('safe-stable-stringify')-const LRUCache = require('mnemonist/lru-cache')+const joi = require('joi') -const kCacheSize = Symbol('kCacheSize')-const kTTL = Symbol('kTTL')-const kOnHit = Symbol('kOnHit')+const cacheOptionsValidation = joi.object({+  storage: joi.object().required(),+  ttl: joi.number().integer().min(0).default(0),+  onDedupe: joi.func().default(() => noop),+  onHit: joi.func().default(() => noop),+  onMiss: joi.func().default(() => noop)+})  class Cache {+  /**+   * @param {!Object} opts+   * @param {!Storage} opts.storage - the storage 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.onHit+   * @param {?function} opts.onMiss+   */   constructor (opts) {-    opts = opts || {}+    const { value: options, error } = cacheOptionsValidation.validate(opts || {})+    if (error) { throw error }+     this[kValues] = {}-    this[kCacheSize] = opts.cacheSize || 1024-    this[kTTL] = opts.ttl || 0-    this[kOnHit] = opts.onHit || noop+    this[kStorage] = options.storage+    this[kTTL] = options.ttl+    this[kOnDedupe] = options.onDedupe+    this[kOnHit] = options.onHit+    this[kOnMiss] = options.onMiss   } -  define (key, opts, func) {+  /**+   * add a new function to dedupe (and cache)+   * @param {!string} name name of the function+   * @param {?Object} [opts]+   * @param {?function} [opts.storage] storage to use; default is the one passed to the constructor+   * @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.onHit] function to call on hit; default is the one passed to the constructor+   * @param {?function} [opts.onMiss] function to call on miss; default is the one passed to the constructor+   * @param {?function} [opts.serialize] custom function to serialize the arguments of `func`, in order to create the key for deduping and caching+   * @param {?function} [opts.references] function to generate references+   * @param {!function} func the function to dedupe (and cache)+   **/+  define (name, opts, func) {     if (typeof opts === 'function') {       func = opts       opts = {}     } -    if (key && this[key]) {-      throw new Error(`${key} is already defined in the cache or it is a forbidden name`)+    if (name && this[name]) {+      throw new Error(`${name} is already defined in the cache or it is a forbidden name`)     }      opts = opts || {}      if (typeof func !== 'function') {-      throw new TypeError(`Missing the function parameter for '${key}'`)+      throw new TypeError(`Missing the function parameter for '${name}'`)     }      const serialize = opts.serialize     if (serialize && typeof serialize !== 'function') {       throw new TypeError('serialize must be a function')     } -    const cacheSize = opts.cacheSize || this[kCacheSize]+    const references = opts.references+    if (references && typeof references !== 'function') {+      throw new TypeError('references must be a function')+    }++    // TODO doc we could even have a different storage for each key+    const storage = opts.storage || this[kStorage]     const ttl = opts.ttl || this[kTTL]+    const onDedupe = opts.onDedupe || this[kOnDedupe]     const onHit = opts.onHit || this[kOnHit]+    const onMiss = opts.onMiss || this[kOnMiss] -    const wrapper = new Wrapper(func, key, serialize, cacheSize, ttl, onHit)+    const wrapper = new Wrapper(func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) -    this[kValues][key] = wrapper-    this[key] = wrapper.add.bind(wrapper)+    this[kValues][name] = wrapper+    this[name] = wrapper.add.bind(wrapper)   } -  clear (key, value) {-    if (key) {-      this[kValues][key].clear(value)+  async clear (name, value) {+    if (name) {+      if (!this[kValues][name]) {+        throw new Error(`${name} is not defined in the cache`)+      }++      await this[kValues][name].clear(value)       return     } +    const clears = []     for (const wrapper of Object.values(this[kValues])) {-      wrapper.clear()+      clears.push(wrapper.clear())     }+    await Promise.all(clears)   }-} -let _currentSecond+  async get (name, key) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key? -function currentSecond () {-  if (_currentSecond !== undefined) {-    return _currentSecond+    return this[kValues][name].get(key)   }-  _currentSecond = Math.floor(Date.now() / 1000)-  setTimeout(_clearSecond, 1000).unref()-  return _currentSecond-} -function _clearSecond () {-  _currentSecond = undefined+  async set (name, key, value, ttl, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate key, value, ttl, references?++    return this[kValues][name].set(key, value, ttl, references)+  }++  async invalidate (name, references) {+    if (!this[kValues][name]) {+      throw new Error(`${name} is not defined in the cache`)+    }++    // TODO validate references?++    return this[kValues][name].invalidate(references)+  } }  class Wrapper {-  constructor (func, key, serialize, cacheSize, ttl, onHit) {-    this.ids = new LRUCache(cacheSize)-    this.error = null-    this.started = false+  // TODO signature+  constructor (func, name, serialize, references, storage, ttl, onDedupe, onHit, onMiss) {+    this.dedupes = new Map()     this.func = func-    this.key = key+    this.name = name     this.serialize = serialize+    this.references = references++    this.storage = storage     this.ttl = ttl+    this.onDedupe = onDedupe     this.onHit = onHit-  }--  buildPromise (query, args, key) {-    query.promise = this.func(args, key)-    // we fork the promise chain on purpose-    const p = query.promise.catch(() => this.ids.set(key, undefined))-    if (this.ttl > 0) {-      query.cachedOn = currentSecond()-    } else {-      // clear the cache if there is no TTL-      p.then(() => this.ids.set(key, undefined))-    }+    this.onMiss = onMiss   }    getKey (args) {     const id = this.serialize ? this.serialize(args) : args     return typeof id === 'string' ? id : stringify(id)   } +  getStorageKey (key) {+    return `${this.name}~${key}`+  }++  getStorageName () {+    return `${this.name}~`+  }+   add (args) {     const key = this.getKey(args)-    const onHit = this.onHit -    let query = this.ids.get(key)+    let query = this.dedupes.get(key)     if (!query) {       query = new Query()       this.buildPromise(query, args, key)-      this.ids.set(key, query)-    } else if (this.ttl > 0) {-      onHit()-      if (currentSecond() - query.cachedOn > this.ttl) {-        // restart-        this.buildPromise(query, args, key)-      }+      this.dedupes.set(key, query)     } else {-      onHit()+      this.onDedupe(key)     }      return query.promise   } -  clear (value) {+  /**+   * wrap the original func to sync storage+   */+  async wrapFunction (args, key) {+    const storageKey = this.getStorageKey(key)+    const data = await this.storage.get(storageKey)+    if (data !== undefined) {+      this.onHit(key)+      return data+    }++    this.onMiss(key)++    const result = await this.func(args, key)++    if (this.ttl < 1) {+      return result+    }++    if (!this.references) {+      await this.storage.set(storageKey, result, this.ttl)+      return result+    }++    const references = await this.references(args, key, result)

avoid the await if the returned value is not a promise

simone-sanfratello

comment created time in a day

Pull request review commentmcollina/async-cache-dedupe

feat: use storage and invalidation

+'use strict'++const joi = require('joi')

please avoid joi

simone-sanfratello

comment created time in a day

more