apicache.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. var url = require('url')
  2. var MemoryCache = require('./memory-cache')
  3. var t = {
  4. ms: 1,
  5. second: 1000,
  6. minute: 60000,
  7. hour: 3600000,
  8. day: 3600000 * 24,
  9. week: 3600000 * 24 * 7,
  10. month: 3600000 * 24 * 30,
  11. }
  12. var instances = []
  13. var matches = function (a) {
  14. return function (b) {
  15. return a === b
  16. }
  17. }
  18. var doesntMatch = function (a) {
  19. return function (b) {
  20. return !matches(a)(b)
  21. }
  22. }
  23. var logDuration = function (d, prefix) {
  24. var str = d > 1000 ? (d / 1000).toFixed(2) + 'sec' : d + 'ms'
  25. return '\x1b[33m- ' + (prefix ? prefix + ' ' : '') + str + '\x1b[0m'
  26. }
  27. function getSafeHeaders(res) {
  28. return res.getHeaders ? res.getHeaders() : res._headers
  29. }
  30. function ApiCache() {
  31. var memCache = new MemoryCache()
  32. var globalOptions = {
  33. debug: false,
  34. defaultDuration: 3600000,
  35. enabled: true,
  36. appendKey: [],
  37. jsonp: false,
  38. redisClient: false,
  39. headerBlacklist: [],
  40. statusCodes: {
  41. include: [],
  42. exclude: [],
  43. },
  44. events: {
  45. expire: undefined,
  46. },
  47. headers: {
  48. // 'cache-control': 'no-cache' // example of header overwrite
  49. },
  50. trackPerformance: false,
  51. }
  52. var middlewareOptions = []
  53. var instance = this
  54. var index = null
  55. var timers = {}
  56. var performanceArray = [] // for tracking cache hit rate
  57. instances.push(this)
  58. this.id = instances.length
  59. function debug(a, b, c, d) {
  60. var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function (arg) {
  61. return arg !== undefined
  62. })
  63. var debugEnv =
  64. process.env.DEBUG &&
  65. process.env.DEBUG.split(',').indexOf('apicache') !== -1
  66. return (globalOptions.debug || debugEnv) && console.log.apply(null, arr)
  67. }
  68. function shouldCacheResponse(request, response, toggle) {
  69. var opt = globalOptions
  70. var codes = opt.statusCodes
  71. if (!response) return false
  72. if (toggle && !toggle(request, response)) {
  73. return false
  74. }
  75. if (
  76. codes.exclude &&
  77. codes.exclude.length &&
  78. codes.exclude.indexOf(response.statusCode) !== -1
  79. )
  80. return false
  81. if (
  82. codes.include &&
  83. codes.include.length &&
  84. codes.include.indexOf(response.statusCode) === -1
  85. )
  86. return false
  87. return true
  88. }
  89. function addIndexEntries(key, req) {
  90. var groupName = req.apicacheGroup
  91. if (groupName) {
  92. debug('group detected "' + groupName + '"')
  93. var group = (index.groups[groupName] = index.groups[groupName] || [])
  94. group.unshift(key)
  95. }
  96. index.all.unshift(key)
  97. }
  98. function filterBlacklistedHeaders(headers) {
  99. return Object.keys(headers)
  100. .filter(function (key) {
  101. return globalOptions.headerBlacklist.indexOf(key) === -1
  102. })
  103. .reduce(function (acc, header) {
  104. acc[header] = headers[header]
  105. return acc
  106. }, {})
  107. }
  108. function createCacheObject(status, headers, data, encoding) {
  109. return {
  110. status: status,
  111. headers: filterBlacklistedHeaders(headers),
  112. data: data,
  113. encoding: encoding,
  114. timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
  115. }
  116. }
  117. function cacheResponse(key, value, duration) {
  118. var redis = globalOptions.redisClient
  119. var expireCallback = globalOptions.events.expire
  120. if (redis && redis.connected) {
  121. try {
  122. redis.hset(key, 'response', JSON.stringify(value))
  123. redis.hset(key, 'duration', duration)
  124. redis.expire(key, duration / 1000, expireCallback || function () {})
  125. } catch (err) {
  126. debug('[apicache] error in redis.hset()')
  127. }
  128. } else {
  129. memCache.add(key, value, duration, expireCallback)
  130. }
  131. // add automatic cache clearing from duration, includes max limit on setTimeout
  132. timers[key] = setTimeout(function () {
  133. instance.clear(key, true)
  134. }, Math.min(duration, 2147483647))
  135. }
  136. function accumulateContent(res, content) {
  137. if (content) {
  138. if (typeof content == 'string') {
  139. res._apicache.content = (res._apicache.content || '') + content
  140. } else if (Buffer.isBuffer(content)) {
  141. var oldContent = res._apicache.content
  142. if (typeof oldContent === 'string') {
  143. oldContent = !Buffer.from
  144. ? new Buffer(oldContent)
  145. : Buffer.from(oldContent)
  146. }
  147. if (!oldContent) {
  148. oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0)
  149. }
  150. res._apicache.content = Buffer.concat(
  151. [oldContent, content],
  152. oldContent.length + content.length,
  153. )
  154. } else {
  155. res._apicache.content = content
  156. }
  157. }
  158. }
  159. function makeResponseCacheable(
  160. req,
  161. res,
  162. next,
  163. key,
  164. duration,
  165. strDuration,
  166. toggle,
  167. ) {
  168. // monkeypatch res.end to create cache object
  169. res._apicache = {
  170. write: res.write,
  171. writeHead: res.writeHead,
  172. end: res.end,
  173. cacheable: true,
  174. content: undefined,
  175. }
  176. // append header overwrites if applicable
  177. Object.keys(globalOptions.headers).forEach(function (name) {
  178. res.setHeader(name, globalOptions.headers[name])
  179. })
  180. res.writeHead = function () {
  181. // add cache control headers
  182. if (!globalOptions.headers['cache-control']) {
  183. if (shouldCacheResponse(req, res, toggle)) {
  184. res.setHeader(
  185. 'cache-control',
  186. 'max-age=' + (duration / 1000).toFixed(0),
  187. )
  188. } else {
  189. res.setHeader('cache-control', 'no-cache, no-store, must-revalidate')
  190. }
  191. }
  192. res._apicache.headers = Object.assign({}, getSafeHeaders(res))
  193. return res._apicache.writeHead.apply(this, arguments)
  194. }
  195. // patch res.write
  196. res.write = function (content) {
  197. accumulateContent(res, content)
  198. return res._apicache.write.apply(this, arguments)
  199. }
  200. // patch res.end
  201. res.end = function (content, encoding) {
  202. if (shouldCacheResponse(req, res, toggle)) {
  203. accumulateContent(res, content)
  204. if (res._apicache.cacheable && res._apicache.content) {
  205. addIndexEntries(key, req)
  206. var headers = res._apicache.headers || getSafeHeaders(res)
  207. var cacheObject = createCacheObject(
  208. res.statusCode,
  209. headers,
  210. res._apicache.content,
  211. encoding,
  212. )
  213. cacheResponse(key, cacheObject, duration)
  214. // display log entry
  215. var elapsed = new Date() - req.apicacheTimer
  216. debug(
  217. 'adding cache entry for "' + key + '" @ ' + strDuration,
  218. logDuration(elapsed),
  219. )
  220. debug('_apicache.headers: ', res._apicache.headers)
  221. debug('res.getHeaders(): ', getSafeHeaders(res))
  222. debug('cacheObject: ', cacheObject)
  223. }
  224. }
  225. return res._apicache.end.apply(this, arguments)
  226. }
  227. next()
  228. }
  229. function sendCachedResponse(
  230. request,
  231. response,
  232. cacheObject,
  233. toggle,
  234. next,
  235. duration,
  236. ) {
  237. if (toggle && !toggle(request, response)) {
  238. return next()
  239. }
  240. var headers = getSafeHeaders(response)
  241. Object.assign(
  242. headers,
  243. filterBlacklistedHeaders(cacheObject.headers || {}),
  244. {
  245. // set properly-decremented max-age header. This ensures that max-age is in sync with the cache expiration.
  246. 'cache-control':
  247. 'max-age=' +
  248. Math.max(
  249. 0,
  250. (
  251. duration / 1000 -
  252. (new Date().getTime() / 1000 - cacheObject.timestamp)
  253. ).toFixed(0),
  254. ),
  255. },
  256. )
  257. // only embed apicache headers when not in production environment
  258. // unstringify buffers
  259. var data = cacheObject.data
  260. if (data && data.type === 'Buffer') {
  261. data =
  262. typeof data.data === 'number'
  263. ? new Buffer.alloc(data.data)
  264. : new Buffer.from(data.data)
  265. }
  266. // test Etag against If-None-Match for 304
  267. var cachedEtag = cacheObject.headers.etag
  268. var requestEtag = request.headers['if-none-match']
  269. if (requestEtag && cachedEtag === requestEtag) {
  270. response.writeHead(304, headers)
  271. return response.end()
  272. }
  273. response.writeHead(cacheObject.status || 200, headers)
  274. return response.end(data, cacheObject.encoding)
  275. }
  276. function syncOptions() {
  277. for (var i in middlewareOptions) {
  278. Object.assign(
  279. middlewareOptions[i].options,
  280. globalOptions,
  281. middlewareOptions[i].localOptions,
  282. )
  283. }
  284. }
  285. this.clear = function (target, isAutomatic) {
  286. var group = index.groups[target]
  287. var redis = globalOptions.redisClient
  288. if (group) {
  289. debug('clearing group "' + target + '"')
  290. group.forEach(function (key) {
  291. debug('clearing cached entry for "' + key + '"')
  292. clearTimeout(timers[key])
  293. delete timers[key]
  294. if (!globalOptions.redisClient) {
  295. memCache.delete(key)
  296. } else {
  297. try {
  298. redis.del(key)
  299. } catch (err) {
  300. console.log('[apicache] error in redis.del("' + key + '")')
  301. }
  302. }
  303. index.all = index.all.filter(doesntMatch(key))
  304. })
  305. delete index.groups[target]
  306. } else if (target) {
  307. debug(
  308. 'clearing ' +
  309. (isAutomatic ? 'expired' : 'cached') +
  310. ' entry for "' +
  311. target +
  312. '"',
  313. )
  314. clearTimeout(timers[target])
  315. delete timers[target]
  316. // clear actual cached entry
  317. if (!redis) {
  318. memCache.delete(target)
  319. } else {
  320. try {
  321. redis.del(target)
  322. } catch (err) {
  323. console.log('[apicache] error in redis.del("' + target + '")')
  324. }
  325. }
  326. // remove from global index
  327. index.all = index.all.filter(doesntMatch(target))
  328. // remove target from each group that it may exist in
  329. Object.keys(index.groups).forEach(function (groupName) {
  330. index.groups[groupName] = index.groups[groupName].filter(
  331. doesntMatch(target),
  332. )
  333. // delete group if now empty
  334. if (!index.groups[groupName].length) {
  335. delete index.groups[groupName]
  336. }
  337. })
  338. } else {
  339. debug('clearing entire index')
  340. if (!redis) {
  341. memCache.clear()
  342. } else {
  343. // clear redis keys one by one from internal index to prevent clearing non-apicache entries
  344. index.all.forEach(function (key) {
  345. clearTimeout(timers[key])
  346. delete timers[key]
  347. try {
  348. redis.del(key)
  349. } catch (err) {
  350. console.log('[apicache] error in redis.del("' + key + '")')
  351. }
  352. })
  353. }
  354. this.resetIndex()
  355. }
  356. return this.getIndex()
  357. }
  358. function parseDuration(duration, defaultDuration) {
  359. if (typeof duration === 'number') return duration
  360. if (typeof duration === 'string') {
  361. var split = duration.match(/^([\d\.,]+)\s?(\w+)$/)
  362. if (split.length === 3) {
  363. var len = parseFloat(split[1])
  364. var unit = split[2].replace(/s$/i, '').toLowerCase()
  365. if (unit === 'm') {
  366. unit = 'ms'
  367. }
  368. return (len || 1) * (t[unit] || 0)
  369. }
  370. }
  371. return defaultDuration
  372. }
  373. this.getDuration = function (duration) {
  374. return parseDuration(duration, globalOptions.defaultDuration)
  375. }
  376. /**
  377. * Return cache performance statistics (hit rate). Suitable for putting into a route:
  378. * <code>
  379. * app.get('/api/cache/performance', (req, res) => {
  380. * res.json(apicache.getPerformance())
  381. * })
  382. * </code>
  383. */
  384. this.getPerformance = function () {
  385. return performanceArray.map(function (p) {
  386. return p.report()
  387. })
  388. }
  389. this.getIndex = function (group) {
  390. if (group) {
  391. return index.groups[group]
  392. } else {
  393. return index
  394. }
  395. }
  396. this.middleware = function cache(
  397. strDuration,
  398. middlewareToggle,
  399. localOptions,
  400. ) {
  401. var duration = instance.getDuration(strDuration)
  402. var opt = {}
  403. middlewareOptions.push({
  404. options: opt,
  405. })
  406. var options = function (localOptions) {
  407. if (localOptions) {
  408. middlewareOptions.find(function (middleware) {
  409. return middleware.options === opt
  410. }).localOptions = localOptions
  411. }
  412. syncOptions()
  413. return opt
  414. }
  415. options(localOptions)
  416. /**
  417. * A Function for non tracking performance
  418. */
  419. function NOOPCachePerformance() {
  420. this.report = this.hit = this.miss = function () {} // noop;
  421. }
  422. /**
  423. * A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
  424. */
  425. function CachePerformance() {
  426. /**
  427. * Tracks the hit rate for the last 100 requests.
  428. * If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
  429. */
  430. this.hitsLast100 = new Uint8Array(100 / 4) // each hit is 2 bits
  431. /**
  432. * Tracks the hit rate for the last 1000 requests.
  433. * If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
  434. */
  435. this.hitsLast1000 = new Uint8Array(1000 / 4) // each hit is 2 bits
  436. /**
  437. * Tracks the hit rate for the last 10000 requests.
  438. * If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
  439. */
  440. this.hitsLast10000 = new Uint8Array(10000 / 4) // each hit is 2 bits
  441. /**
  442. * Tracks the hit rate for the last 100000 requests.
  443. * If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
  444. */
  445. this.hitsLast100000 = new Uint8Array(100000 / 4) // each hit is 2 bits
  446. /**
  447. * The number of calls that have passed through the middleware since the server started.
  448. */
  449. this.callCount = 0
  450. /**
  451. * The total number of hits since the server started
  452. */
  453. this.hitCount = 0
  454. /**
  455. * The key from the last cache hit. This is useful in identifying which route these statistics apply to.
  456. */
  457. this.lastCacheHit = null
  458. /**
  459. * The key from the last cache miss. This is useful in identifying which route these statistics apply to.
  460. */
  461. this.lastCacheMiss = null
  462. /**
  463. * Return performance statistics
  464. */
  465. this.report = function () {
  466. return {
  467. lastCacheHit: this.lastCacheHit,
  468. lastCacheMiss: this.lastCacheMiss,
  469. callCount: this.callCount,
  470. hitCount: this.hitCount,
  471. missCount: this.callCount - this.hitCount,
  472. hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
  473. hitRateLast100: this.hitRate(this.hitsLast100),
  474. hitRateLast1000: this.hitRate(this.hitsLast1000),
  475. hitRateLast10000: this.hitRate(this.hitsLast10000),
  476. hitRateLast100000: this.hitRate(this.hitsLast100000),
  477. }
  478. }
  479. /**
  480. * Computes a cache hit rate from an array of hits and misses.
  481. * @param {Uint8Array} array An array representing hits and misses.
  482. * @returns a number between 0 and 1, or null if the array has no hits or misses
  483. */
  484. this.hitRate = function (array) {
  485. var hits = 0
  486. var misses = 0
  487. for (var i = 0; i < array.length; i++) {
  488. var n8 = array[i]
  489. for (j = 0; j < 4; j++) {
  490. switch (n8 & 3) {
  491. case 1:
  492. hits++
  493. break
  494. case 2:
  495. misses++
  496. break
  497. }
  498. n8 >>= 2
  499. }
  500. }
  501. var total = hits + misses
  502. if (total == 0) return null
  503. return hits / total
  504. }
  505. /**
  506. * Record a hit or miss in the given array. It will be recorded at a position determined
  507. * by the current value of the callCount variable.
  508. * @param {Uint8Array} array An array representing hits and misses.
  509. * @param {boolean} hit true for a hit, false for a miss
  510. * Each element in the array is 8 bits, and encodes 4 hit/miss records.
  511. * Each hit or miss is encoded as to bits as follows:
  512. * 00 means no hit or miss has been recorded in these bits
  513. * 01 encodes a hit
  514. * 10 encodes a miss
  515. */
  516. this.recordHitInArray = function (array, hit) {
  517. var arrayIndex = ~~(this.callCount / 4) % array.length
  518. var bitOffset = (this.callCount % 4) * 2 // 2 bits per record, 4 records per uint8 array element
  519. var clearMask = ~(3 << bitOffset)
  520. var record = (hit ? 1 : 2) << bitOffset
  521. array[arrayIndex] = (array[arrayIndex] & clearMask) | record
  522. }
  523. /**
  524. * Records the hit or miss in the tracking arrays and increments the call count.
  525. * @param {boolean} hit true records a hit, false records a miss
  526. */
  527. this.recordHit = function (hit) {
  528. this.recordHitInArray(this.hitsLast100, hit)
  529. this.recordHitInArray(this.hitsLast1000, hit)
  530. this.recordHitInArray(this.hitsLast10000, hit)
  531. this.recordHitInArray(this.hitsLast100000, hit)
  532. if (hit) this.hitCount++
  533. this.callCount++
  534. }
  535. /**
  536. * Records a hit event, setting lastCacheMiss to the given key
  537. * @param {string} key The key that had the cache hit
  538. */
  539. this.hit = function (key) {
  540. this.recordHit(true)
  541. this.lastCacheHit = key
  542. }
  543. /**
  544. * Records a miss event, setting lastCacheMiss to the given key
  545. * @param {string} key The key that had the cache miss
  546. */
  547. this.miss = function (key) {
  548. this.recordHit(false)
  549. this.lastCacheMiss = key
  550. }
  551. }
  552. var perf = globalOptions.trackPerformance
  553. ? new CachePerformance()
  554. : new NOOPCachePerformance()
  555. performanceArray.push(perf)
  556. var cache = function (req, res, next) {
  557. function bypass() {
  558. debug('bypass detected, skipping cache.')
  559. return next()
  560. }
  561. // initial bypass chances
  562. if (!opt.enabled) return bypass()
  563. if (
  564. req.headers['x-apicache-bypass'] ||
  565. req.headers['x-apicache-force-fetch']
  566. )
  567. return bypass()
  568. // REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
  569. // if (typeof middlewareToggle === 'function') {
  570. // if (!middlewareToggle(req, res)) return bypass()
  571. // } else if (middlewareToggle !== undefined && !middlewareToggle) {
  572. // return bypass()
  573. // }
  574. // embed timer
  575. req.apicacheTimer = new Date()
  576. // In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
  577. var key =
  578. req.hostname +
  579. (req.originalUrl || req.url) +
  580. JSON.stringify(req.cookies)
  581. // Remove querystring from key if jsonp option is enabled
  582. if (opt.jsonp) {
  583. key = url.parse(key).pathname
  584. }
  585. // add appendKey (either custom function or response path)
  586. if (typeof opt.appendKey === 'function') {
  587. key += '$$appendKey=' + opt.appendKey(req, res)
  588. } else if (opt.appendKey.length > 0) {
  589. var appendKey = req
  590. for (var i = 0; i < opt.appendKey.length; i++) {
  591. appendKey = appendKey[opt.appendKey[i]]
  592. }
  593. key += '$$appendKey=' + appendKey
  594. }
  595. // attempt cache hit
  596. var redis = opt.redisClient
  597. var cached = !redis ? memCache.getValue(key) : null
  598. // send if cache hit from memory-cache
  599. if (cached) {
  600. var elapsed = new Date() - req.apicacheTimer
  601. debug(
  602. 'sending cached (memory-cache) version of',
  603. key,
  604. logDuration(elapsed),
  605. )
  606. perf.hit(key)
  607. return sendCachedResponse(
  608. req,
  609. res,
  610. cached,
  611. middlewareToggle,
  612. next,
  613. duration,
  614. )
  615. }
  616. // send if cache hit from redis
  617. if (redis && redis.connected) {
  618. try {
  619. redis.hgetall(key, function (err, obj) {
  620. if (!err && obj && obj.response) {
  621. var elapsed = new Date() - req.apicacheTimer
  622. debug(
  623. 'sending cached (redis) version of',
  624. key,
  625. logDuration(elapsed),
  626. )
  627. perf.hit(key)
  628. return sendCachedResponse(
  629. req,
  630. res,
  631. JSON.parse(obj.response),
  632. middlewareToggle,
  633. next,
  634. duration,
  635. )
  636. } else {
  637. perf.miss(key)
  638. return makeResponseCacheable(
  639. req,
  640. res,
  641. next,
  642. key,
  643. duration,
  644. strDuration,
  645. middlewareToggle,
  646. )
  647. }
  648. })
  649. } catch (err) {
  650. // bypass redis on error
  651. perf.miss(key)
  652. return makeResponseCacheable(
  653. req,
  654. res,
  655. next,
  656. key,
  657. duration,
  658. strDuration,
  659. middlewareToggle,
  660. )
  661. }
  662. } else {
  663. perf.miss(key)
  664. return makeResponseCacheable(
  665. req,
  666. res,
  667. next,
  668. key,
  669. duration,
  670. strDuration,
  671. middlewareToggle,
  672. )
  673. }
  674. }
  675. cache.options = options
  676. return cache
  677. }
  678. this.options = function (options) {
  679. if (options) {
  680. Object.assign(globalOptions, options)
  681. syncOptions()
  682. if ('defaultDuration' in options) {
  683. // Convert the default duration to a number in milliseconds (if needed)
  684. globalOptions.defaultDuration = parseDuration(
  685. globalOptions.defaultDuration,
  686. 3600000,
  687. )
  688. }
  689. if (globalOptions.trackPerformance) {
  690. debug(
  691. 'WARNING: using trackPerformance flag can cause high memory usage!',
  692. )
  693. }
  694. return this
  695. } else {
  696. return globalOptions
  697. }
  698. }
  699. this.resetIndex = function () {
  700. index = {
  701. all: [],
  702. groups: {},
  703. }
  704. }
  705. this.newInstance = function (config) {
  706. var instance = new ApiCache()
  707. if (config) {
  708. instance.options(config)
  709. }
  710. return instance
  711. }
  712. this.clone = function () {
  713. return this.newInstance(this.options())
  714. }
  715. // initialize index
  716. this.resetIndex()
  717. }
  718. module.exports = new ApiCache()