index.mjs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227
  1. const perf =
  2. typeof performance === 'object' &&
  3. performance &&
  4. typeof performance.now === 'function'
  5. ? performance
  6. : Date
  7. const hasAbortController = typeof AbortController === 'function'
  8. // minimal backwards-compatibility polyfill
  9. // this doesn't have nearly all the checks and whatnot that
  10. // actual AbortController/Signal has, but it's enough for
  11. // our purposes, and if used properly, behaves the same.
  12. const AC = hasAbortController
  13. ? AbortController
  14. : class AbortController {
  15. constructor() {
  16. this.signal = new AS()
  17. }
  18. abort(reason = new Error('This operation was aborted')) {
  19. this.signal.reason = this.signal.reason || reason
  20. this.signal.aborted = true
  21. this.signal.dispatchEvent({
  22. type: 'abort',
  23. target: this.signal,
  24. })
  25. }
  26. }
  27. const hasAbortSignal = typeof AbortSignal === 'function'
  28. // Some polyfills put this on the AC class, not global
  29. const hasACAbortSignal = typeof AC.AbortSignal === 'function'
  30. const AS = hasAbortSignal
  31. ? AbortSignal
  32. : hasACAbortSignal
  33. ? AC.AbortController
  34. : class AbortSignal {
  35. constructor() {
  36. this.reason = undefined
  37. this.aborted = false
  38. this._listeners = []
  39. }
  40. dispatchEvent(e) {
  41. if (e.type === 'abort') {
  42. this.aborted = true
  43. this.onabort(e)
  44. this._listeners.forEach(f => f(e), this)
  45. }
  46. }
  47. onabort() {}
  48. addEventListener(ev, fn) {
  49. if (ev === 'abort') {
  50. this._listeners.push(fn)
  51. }
  52. }
  53. removeEventListener(ev, fn) {
  54. if (ev === 'abort') {
  55. this._listeners = this._listeners.filter(f => f !== fn)
  56. }
  57. }
  58. }
  59. const warned = new Set()
  60. const deprecatedOption = (opt, instead) => {
  61. const code = `LRU_CACHE_OPTION_${opt}`
  62. if (shouldWarn(code)) {
  63. warn(code, `${opt} option`, `options.${instead}`, LRUCache)
  64. }
  65. }
  66. const deprecatedMethod = (method, instead) => {
  67. const code = `LRU_CACHE_METHOD_${method}`
  68. if (shouldWarn(code)) {
  69. const { prototype } = LRUCache
  70. const { get } = Object.getOwnPropertyDescriptor(prototype, method)
  71. warn(code, `${method} method`, `cache.${instead}()`, get)
  72. }
  73. }
  74. const deprecatedProperty = (field, instead) => {
  75. const code = `LRU_CACHE_PROPERTY_${field}`
  76. if (shouldWarn(code)) {
  77. const { prototype } = LRUCache
  78. const { get } = Object.getOwnPropertyDescriptor(prototype, field)
  79. warn(code, `${field} property`, `cache.${instead}`, get)
  80. }
  81. }
  82. const emitWarning = (...a) => {
  83. typeof process === 'object' &&
  84. process &&
  85. typeof process.emitWarning === 'function'
  86. ? process.emitWarning(...a)
  87. : console.error(...a)
  88. }
  89. const shouldWarn = code => !warned.has(code)
  90. const warn = (code, what, instead, fn) => {
  91. warned.add(code)
  92. const msg = `The ${what} is deprecated. Please use ${instead} instead.`
  93. emitWarning(msg, 'DeprecationWarning', code, fn)
  94. }
  95. const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n)
  96. /* istanbul ignore next - This is a little bit ridiculous, tbh.
  97. * The maximum array length is 2^32-1 or thereabouts on most JS impls.
  98. * And well before that point, you're caching the entire world, I mean,
  99. * that's ~32GB of just integers for the next/prev links, plus whatever
  100. * else to hold that many keys and values. Just filling the memory with
  101. * zeroes at init time is brutal when you get that big.
  102. * But why not be complete?
  103. * Maybe in the future, these limits will have expanded. */
  104. const getUintArray = max =>
  105. !isPosInt(max)
  106. ? null
  107. : max <= Math.pow(2, 8)
  108. ? Uint8Array
  109. : max <= Math.pow(2, 16)
  110. ? Uint16Array
  111. : max <= Math.pow(2, 32)
  112. ? Uint32Array
  113. : max <= Number.MAX_SAFE_INTEGER
  114. ? ZeroArray
  115. : null
  116. class ZeroArray extends Array {
  117. constructor(size) {
  118. super(size)
  119. this.fill(0)
  120. }
  121. }
  122. class Stack {
  123. constructor(max) {
  124. if (max === 0) {
  125. return []
  126. }
  127. const UintArray = getUintArray(max)
  128. this.heap = new UintArray(max)
  129. this.length = 0
  130. }
  131. push(n) {
  132. this.heap[this.length++] = n
  133. }
  134. pop() {
  135. return this.heap[--this.length]
  136. }
  137. }
  138. class LRUCache {
  139. constructor(options = {}) {
  140. const {
  141. max = 0,
  142. ttl,
  143. ttlResolution = 1,
  144. ttlAutopurge,
  145. updateAgeOnGet,
  146. updateAgeOnHas,
  147. allowStale,
  148. dispose,
  149. disposeAfter,
  150. noDisposeOnSet,
  151. noUpdateTTL,
  152. maxSize = 0,
  153. maxEntrySize = 0,
  154. sizeCalculation,
  155. fetchMethod,
  156. fetchContext,
  157. noDeleteOnFetchRejection,
  158. noDeleteOnStaleGet,
  159. allowStaleOnFetchRejection,
  160. allowStaleOnFetchAbort,
  161. ignoreFetchAbort,
  162. } = options
  163. // deprecated options, don't trigger a warning for getting them if
  164. // the thing being passed in is another LRUCache we're copying.
  165. const { length, maxAge, stale } =
  166. options instanceof LRUCache ? {} : options
  167. if (max !== 0 && !isPosInt(max)) {
  168. throw new TypeError('max option must be a nonnegative integer')
  169. }
  170. const UintArray = max ? getUintArray(max) : Array
  171. if (!UintArray) {
  172. throw new Error('invalid max value: ' + max)
  173. }
  174. this.max = max
  175. this.maxSize = maxSize
  176. this.maxEntrySize = maxEntrySize || this.maxSize
  177. this.sizeCalculation = sizeCalculation || length
  178. if (this.sizeCalculation) {
  179. if (!this.maxSize && !this.maxEntrySize) {
  180. throw new TypeError(
  181. 'cannot set sizeCalculation without setting maxSize or maxEntrySize'
  182. )
  183. }
  184. if (typeof this.sizeCalculation !== 'function') {
  185. throw new TypeError('sizeCalculation set to non-function')
  186. }
  187. }
  188. this.fetchMethod = fetchMethod || null
  189. if (this.fetchMethod && typeof this.fetchMethod !== 'function') {
  190. throw new TypeError(
  191. 'fetchMethod must be a function if specified'
  192. )
  193. }
  194. this.fetchContext = fetchContext
  195. if (!this.fetchMethod && fetchContext !== undefined) {
  196. throw new TypeError(
  197. 'cannot set fetchContext without fetchMethod'
  198. )
  199. }
  200. this.keyMap = new Map()
  201. this.keyList = new Array(max).fill(null)
  202. this.valList = new Array(max).fill(null)
  203. this.next = new UintArray(max)
  204. this.prev = new UintArray(max)
  205. this.head = 0
  206. this.tail = 0
  207. this.free = new Stack(max)
  208. this.initialFill = 1
  209. this.size = 0
  210. if (typeof dispose === 'function') {
  211. this.dispose = dispose
  212. }
  213. if (typeof disposeAfter === 'function') {
  214. this.disposeAfter = disposeAfter
  215. this.disposed = []
  216. } else {
  217. this.disposeAfter = null
  218. this.disposed = null
  219. }
  220. this.noDisposeOnSet = !!noDisposeOnSet
  221. this.noUpdateTTL = !!noUpdateTTL
  222. this.noDeleteOnFetchRejection = !!noDeleteOnFetchRejection
  223. this.allowStaleOnFetchRejection = !!allowStaleOnFetchRejection
  224. this.allowStaleOnFetchAbort = !!allowStaleOnFetchAbort
  225. this.ignoreFetchAbort = !!ignoreFetchAbort
  226. // NB: maxEntrySize is set to maxSize if it's set
  227. if (this.maxEntrySize !== 0) {
  228. if (this.maxSize !== 0) {
  229. if (!isPosInt(this.maxSize)) {
  230. throw new TypeError(
  231. 'maxSize must be a positive integer if specified'
  232. )
  233. }
  234. }
  235. if (!isPosInt(this.maxEntrySize)) {
  236. throw new TypeError(
  237. 'maxEntrySize must be a positive integer if specified'
  238. )
  239. }
  240. this.initializeSizeTracking()
  241. }
  242. this.allowStale = !!allowStale || !!stale
  243. this.noDeleteOnStaleGet = !!noDeleteOnStaleGet
  244. this.updateAgeOnGet = !!updateAgeOnGet
  245. this.updateAgeOnHas = !!updateAgeOnHas
  246. this.ttlResolution =
  247. isPosInt(ttlResolution) || ttlResolution === 0
  248. ? ttlResolution
  249. : 1
  250. this.ttlAutopurge = !!ttlAutopurge
  251. this.ttl = ttl || maxAge || 0
  252. if (this.ttl) {
  253. if (!isPosInt(this.ttl)) {
  254. throw new TypeError(
  255. 'ttl must be a positive integer if specified'
  256. )
  257. }
  258. this.initializeTTLTracking()
  259. }
  260. // do not allow completely unbounded caches
  261. if (this.max === 0 && this.ttl === 0 && this.maxSize === 0) {
  262. throw new TypeError(
  263. 'At least one of max, maxSize, or ttl is required'
  264. )
  265. }
  266. if (!this.ttlAutopurge && !this.max && !this.maxSize) {
  267. const code = 'LRU_CACHE_UNBOUNDED'
  268. if (shouldWarn(code)) {
  269. warned.add(code)
  270. const msg =
  271. 'TTL caching without ttlAutopurge, max, or maxSize can ' +
  272. 'result in unbounded memory consumption.'
  273. emitWarning(msg, 'UnboundedCacheWarning', code, LRUCache)
  274. }
  275. }
  276. if (stale) {
  277. deprecatedOption('stale', 'allowStale')
  278. }
  279. if (maxAge) {
  280. deprecatedOption('maxAge', 'ttl')
  281. }
  282. if (length) {
  283. deprecatedOption('length', 'sizeCalculation')
  284. }
  285. }
  286. getRemainingTTL(key) {
  287. return this.has(key, { updateAgeOnHas: false }) ? Infinity : 0
  288. }
  289. initializeTTLTracking() {
  290. this.ttls = new ZeroArray(this.max)
  291. this.starts = new ZeroArray(this.max)
  292. this.setItemTTL = (index, ttl, start = perf.now()) => {
  293. this.starts[index] = ttl !== 0 ? start : 0
  294. this.ttls[index] = ttl
  295. if (ttl !== 0 && this.ttlAutopurge) {
  296. const t = setTimeout(() => {
  297. if (this.isStale(index)) {
  298. this.delete(this.keyList[index])
  299. }
  300. }, ttl + 1)
  301. /* istanbul ignore else - unref() not supported on all platforms */
  302. if (t.unref) {
  303. t.unref()
  304. }
  305. }
  306. }
  307. this.updateItemAge = index => {
  308. this.starts[index] = this.ttls[index] !== 0 ? perf.now() : 0
  309. }
  310. this.statusTTL = (status, index) => {
  311. if (status) {
  312. status.ttl = this.ttls[index]
  313. status.start = this.starts[index]
  314. status.now = cachedNow || getNow()
  315. status.remainingTTL = status.now + status.ttl - status.start
  316. }
  317. }
  318. // debounce calls to perf.now() to 1s so we're not hitting
  319. // that costly call repeatedly.
  320. let cachedNow = 0
  321. const getNow = () => {
  322. const n = perf.now()
  323. if (this.ttlResolution > 0) {
  324. cachedNow = n
  325. const t = setTimeout(
  326. () => (cachedNow = 0),
  327. this.ttlResolution
  328. )
  329. /* istanbul ignore else - not available on all platforms */
  330. if (t.unref) {
  331. t.unref()
  332. }
  333. }
  334. return n
  335. }
  336. this.getRemainingTTL = key => {
  337. const index = this.keyMap.get(key)
  338. if (index === undefined) {
  339. return 0
  340. }
  341. return this.ttls[index] === 0 || this.starts[index] === 0
  342. ? Infinity
  343. : this.starts[index] +
  344. this.ttls[index] -
  345. (cachedNow || getNow())
  346. }
  347. this.isStale = index => {
  348. return (
  349. this.ttls[index] !== 0 &&
  350. this.starts[index] !== 0 &&
  351. (cachedNow || getNow()) - this.starts[index] >
  352. this.ttls[index]
  353. )
  354. }
  355. }
  356. updateItemAge(_index) {}
  357. statusTTL(_status, _index) {}
  358. setItemTTL(_index, _ttl, _start) {}
  359. isStale(_index) {
  360. return false
  361. }
  362. initializeSizeTracking() {
  363. this.calculatedSize = 0
  364. this.sizes = new ZeroArray(this.max)
  365. this.removeItemSize = index => {
  366. this.calculatedSize -= this.sizes[index]
  367. this.sizes[index] = 0
  368. }
  369. this.requireSize = (k, v, size, sizeCalculation) => {
  370. // provisionally accept background fetches.
  371. // actual value size will be checked when they return.
  372. if (this.isBackgroundFetch(v)) {
  373. return 0
  374. }
  375. if (!isPosInt(size)) {
  376. if (sizeCalculation) {
  377. if (typeof sizeCalculation !== 'function') {
  378. throw new TypeError('sizeCalculation must be a function')
  379. }
  380. size = sizeCalculation(v, k)
  381. if (!isPosInt(size)) {
  382. throw new TypeError(
  383. 'sizeCalculation return invalid (expect positive integer)'
  384. )
  385. }
  386. } else {
  387. throw new TypeError(
  388. 'invalid size value (must be positive integer). ' +
  389. 'When maxSize or maxEntrySize is used, sizeCalculation or size ' +
  390. 'must be set.'
  391. )
  392. }
  393. }
  394. return size
  395. }
  396. this.addItemSize = (index, size, status) => {
  397. this.sizes[index] = size
  398. if (this.maxSize) {
  399. const maxSize = this.maxSize - this.sizes[index]
  400. while (this.calculatedSize > maxSize) {
  401. this.evict(true)
  402. }
  403. }
  404. this.calculatedSize += this.sizes[index]
  405. if (status) {
  406. status.entrySize = size
  407. status.totalCalculatedSize = this.calculatedSize
  408. }
  409. }
  410. }
  411. removeItemSize(_index) {}
  412. addItemSize(_index, _size) {}
  413. requireSize(_k, _v, size, sizeCalculation) {
  414. if (size || sizeCalculation) {
  415. throw new TypeError(
  416. 'cannot set size without setting maxSize or maxEntrySize on cache'
  417. )
  418. }
  419. }
  420. *indexes({ allowStale = this.allowStale } = {}) {
  421. if (this.size) {
  422. for (let i = this.tail; true; ) {
  423. if (!this.isValidIndex(i)) {
  424. break
  425. }
  426. if (allowStale || !this.isStale(i)) {
  427. yield i
  428. }
  429. if (i === this.head) {
  430. break
  431. } else {
  432. i = this.prev[i]
  433. }
  434. }
  435. }
  436. }
  437. *rindexes({ allowStale = this.allowStale } = {}) {
  438. if (this.size) {
  439. for (let i = this.head; true; ) {
  440. if (!this.isValidIndex(i)) {
  441. break
  442. }
  443. if (allowStale || !this.isStale(i)) {
  444. yield i
  445. }
  446. if (i === this.tail) {
  447. break
  448. } else {
  449. i = this.next[i]
  450. }
  451. }
  452. }
  453. }
  454. isValidIndex(index) {
  455. return (
  456. index !== undefined &&
  457. this.keyMap.get(this.keyList[index]) === index
  458. )
  459. }
  460. *entries() {
  461. for (const i of this.indexes()) {
  462. if (
  463. this.valList[i] !== undefined &&
  464. this.keyList[i] !== undefined &&
  465. !this.isBackgroundFetch(this.valList[i])
  466. ) {
  467. yield [this.keyList[i], this.valList[i]]
  468. }
  469. }
  470. }
  471. *rentries() {
  472. for (const i of this.rindexes()) {
  473. if (
  474. this.valList[i] !== undefined &&
  475. this.keyList[i] !== undefined &&
  476. !this.isBackgroundFetch(this.valList[i])
  477. ) {
  478. yield [this.keyList[i], this.valList[i]]
  479. }
  480. }
  481. }
  482. *keys() {
  483. for (const i of this.indexes()) {
  484. if (
  485. this.keyList[i] !== undefined &&
  486. !this.isBackgroundFetch(this.valList[i])
  487. ) {
  488. yield this.keyList[i]
  489. }
  490. }
  491. }
  492. *rkeys() {
  493. for (const i of this.rindexes()) {
  494. if (
  495. this.keyList[i] !== undefined &&
  496. !this.isBackgroundFetch(this.valList[i])
  497. ) {
  498. yield this.keyList[i]
  499. }
  500. }
  501. }
  502. *values() {
  503. for (const i of this.indexes()) {
  504. if (
  505. this.valList[i] !== undefined &&
  506. !this.isBackgroundFetch(this.valList[i])
  507. ) {
  508. yield this.valList[i]
  509. }
  510. }
  511. }
  512. *rvalues() {
  513. for (const i of this.rindexes()) {
  514. if (
  515. this.valList[i] !== undefined &&
  516. !this.isBackgroundFetch(this.valList[i])
  517. ) {
  518. yield this.valList[i]
  519. }
  520. }
  521. }
  522. [Symbol.iterator]() {
  523. return this.entries()
  524. }
  525. find(fn, getOptions) {
  526. for (const i of this.indexes()) {
  527. const v = this.valList[i]
  528. const value = this.isBackgroundFetch(v)
  529. ? v.__staleWhileFetching
  530. : v
  531. if (value === undefined) continue
  532. if (fn(value, this.keyList[i], this)) {
  533. return this.get(this.keyList[i], getOptions)
  534. }
  535. }
  536. }
  537. forEach(fn, thisp = this) {
  538. for (const i of this.indexes()) {
  539. const v = this.valList[i]
  540. const value = this.isBackgroundFetch(v)
  541. ? v.__staleWhileFetching
  542. : v
  543. if (value === undefined) continue
  544. fn.call(thisp, value, this.keyList[i], this)
  545. }
  546. }
  547. rforEach(fn, thisp = this) {
  548. for (const i of this.rindexes()) {
  549. const v = this.valList[i]
  550. const value = this.isBackgroundFetch(v)
  551. ? v.__staleWhileFetching
  552. : v
  553. if (value === undefined) continue
  554. fn.call(thisp, value, this.keyList[i], this)
  555. }
  556. }
  557. get prune() {
  558. deprecatedMethod('prune', 'purgeStale')
  559. return this.purgeStale
  560. }
  561. purgeStale() {
  562. let deleted = false
  563. for (const i of this.rindexes({ allowStale: true })) {
  564. if (this.isStale(i)) {
  565. this.delete(this.keyList[i])
  566. deleted = true
  567. }
  568. }
  569. return deleted
  570. }
  571. dump() {
  572. const arr = []
  573. for (const i of this.indexes({ allowStale: true })) {
  574. const key = this.keyList[i]
  575. const v = this.valList[i]
  576. const value = this.isBackgroundFetch(v)
  577. ? v.__staleWhileFetching
  578. : v
  579. if (value === undefined) continue
  580. const entry = { value }
  581. if (this.ttls) {
  582. entry.ttl = this.ttls[i]
  583. // always dump the start relative to a portable timestamp
  584. // it's ok for this to be a bit slow, it's a rare operation.
  585. const age = perf.now() - this.starts[i]
  586. entry.start = Math.floor(Date.now() - age)
  587. }
  588. if (this.sizes) {
  589. entry.size = this.sizes[i]
  590. }
  591. arr.unshift([key, entry])
  592. }
  593. return arr
  594. }
  595. load(arr) {
  596. this.clear()
  597. for (const [key, entry] of arr) {
  598. if (entry.start) {
  599. // entry.start is a portable timestamp, but we may be using
  600. // node's performance.now(), so calculate the offset.
  601. // it's ok for this to be a bit slow, it's a rare operation.
  602. const age = Date.now() - entry.start
  603. entry.start = perf.now() - age
  604. }
  605. this.set(key, entry.value, entry)
  606. }
  607. }
  608. dispose(_v, _k, _reason) {}
  609. set(
  610. k,
  611. v,
  612. {
  613. ttl = this.ttl,
  614. start,
  615. noDisposeOnSet = this.noDisposeOnSet,
  616. size = 0,
  617. sizeCalculation = this.sizeCalculation,
  618. noUpdateTTL = this.noUpdateTTL,
  619. status,
  620. } = {}
  621. ) {
  622. size = this.requireSize(k, v, size, sizeCalculation)
  623. // if the item doesn't fit, don't do anything
  624. // NB: maxEntrySize set to maxSize by default
  625. if (this.maxEntrySize && size > this.maxEntrySize) {
  626. if (status) {
  627. status.set = 'miss'
  628. status.maxEntrySizeExceeded = true
  629. }
  630. // have to delete, in case a background fetch is there already.
  631. // in non-async cases, this is a no-op
  632. this.delete(k)
  633. return this
  634. }
  635. let index = this.size === 0 ? undefined : this.keyMap.get(k)
  636. if (index === undefined) {
  637. // addition
  638. index = this.newIndex()
  639. this.keyList[index] = k
  640. this.valList[index] = v
  641. this.keyMap.set(k, index)
  642. this.next[this.tail] = index
  643. this.prev[index] = this.tail
  644. this.tail = index
  645. this.size++
  646. this.addItemSize(index, size, status)
  647. if (status) {
  648. status.set = 'add'
  649. }
  650. noUpdateTTL = false
  651. } else {
  652. // update
  653. this.moveToTail(index)
  654. const oldVal = this.valList[index]
  655. if (v !== oldVal) {
  656. if (this.isBackgroundFetch(oldVal)) {
  657. oldVal.__abortController.abort(new Error('replaced'))
  658. } else {
  659. if (!noDisposeOnSet) {
  660. this.dispose(oldVal, k, 'set')
  661. if (this.disposeAfter) {
  662. this.disposed.push([oldVal, k, 'set'])
  663. }
  664. }
  665. }
  666. this.removeItemSize(index)
  667. this.valList[index] = v
  668. this.addItemSize(index, size, status)
  669. if (status) {
  670. status.set = 'replace'
  671. const oldValue =
  672. oldVal && this.isBackgroundFetch(oldVal)
  673. ? oldVal.__staleWhileFetching
  674. : oldVal
  675. if (oldValue !== undefined) status.oldValue = oldValue
  676. }
  677. } else if (status) {
  678. status.set = 'update'
  679. }
  680. }
  681. if (ttl !== 0 && this.ttl === 0 && !this.ttls) {
  682. this.initializeTTLTracking()
  683. }
  684. if (!noUpdateTTL) {
  685. this.setItemTTL(index, ttl, start)
  686. }
  687. this.statusTTL(status, index)
  688. if (this.disposeAfter) {
  689. while (this.disposed.length) {
  690. this.disposeAfter(...this.disposed.shift())
  691. }
  692. }
  693. return this
  694. }
  695. newIndex() {
  696. if (this.size === 0) {
  697. return this.tail
  698. }
  699. if (this.size === this.max && this.max !== 0) {
  700. return this.evict(false)
  701. }
  702. if (this.free.length !== 0) {
  703. return this.free.pop()
  704. }
  705. // initial fill, just keep writing down the list
  706. return this.initialFill++
  707. }
  708. pop() {
  709. if (this.size) {
  710. const val = this.valList[this.head]
  711. this.evict(true)
  712. return val
  713. }
  714. }
  715. evict(free) {
  716. const head = this.head
  717. const k = this.keyList[head]
  718. const v = this.valList[head]
  719. if (this.isBackgroundFetch(v)) {
  720. v.__abortController.abort(new Error('evicted'))
  721. } else {
  722. this.dispose(v, k, 'evict')
  723. if (this.disposeAfter) {
  724. this.disposed.push([v, k, 'evict'])
  725. }
  726. }
  727. this.removeItemSize(head)
  728. // if we aren't about to use the index, then null these out
  729. if (free) {
  730. this.keyList[head] = null
  731. this.valList[head] = null
  732. this.free.push(head)
  733. }
  734. this.head = this.next[head]
  735. this.keyMap.delete(k)
  736. this.size--
  737. return head
  738. }
  739. has(k, { updateAgeOnHas = this.updateAgeOnHas, status } = {}) {
  740. const index = this.keyMap.get(k)
  741. if (index !== undefined) {
  742. if (!this.isStale(index)) {
  743. if (updateAgeOnHas) {
  744. this.updateItemAge(index)
  745. }
  746. if (status) status.has = 'hit'
  747. this.statusTTL(status, index)
  748. return true
  749. } else if (status) {
  750. status.has = 'stale'
  751. this.statusTTL(status, index)
  752. }
  753. } else if (status) {
  754. status.has = 'miss'
  755. }
  756. return false
  757. }
  758. // like get(), but without any LRU updating or TTL expiration
  759. peek(k, { allowStale = this.allowStale } = {}) {
  760. const index = this.keyMap.get(k)
  761. if (index !== undefined && (allowStale || !this.isStale(index))) {
  762. const v = this.valList[index]
  763. // either stale and allowed, or forcing a refresh of non-stale value
  764. return this.isBackgroundFetch(v) ? v.__staleWhileFetching : v
  765. }
  766. }
  767. backgroundFetch(k, index, options, context) {
  768. const v = index === undefined ? undefined : this.valList[index]
  769. if (this.isBackgroundFetch(v)) {
  770. return v
  771. }
  772. const ac = new AC()
  773. if (options.signal) {
  774. options.signal.addEventListener('abort', () =>
  775. ac.abort(options.signal.reason)
  776. )
  777. }
  778. const fetchOpts = {
  779. signal: ac.signal,
  780. options,
  781. context,
  782. }
  783. const cb = (v, updateCache = false) => {
  784. const { aborted } = ac.signal
  785. const ignoreAbort = options.ignoreFetchAbort && v !== undefined
  786. if (options.status) {
  787. if (aborted && !updateCache) {
  788. options.status.fetchAborted = true
  789. options.status.fetchError = ac.signal.reason
  790. if (ignoreAbort) options.status.fetchAbortIgnored = true
  791. } else {
  792. options.status.fetchResolved = true
  793. }
  794. }
  795. if (aborted && !ignoreAbort && !updateCache) {
  796. return fetchFail(ac.signal.reason)
  797. }
  798. // either we didn't abort, and are still here, or we did, and ignored
  799. if (this.valList[index] === p) {
  800. if (v === undefined) {
  801. if (p.__staleWhileFetching) {
  802. this.valList[index] = p.__staleWhileFetching
  803. } else {
  804. this.delete(k)
  805. }
  806. } else {
  807. if (options.status) options.status.fetchUpdated = true
  808. this.set(k, v, fetchOpts.options)
  809. }
  810. }
  811. return v
  812. }
  813. const eb = er => {
  814. if (options.status) {
  815. options.status.fetchRejected = true
  816. options.status.fetchError = er
  817. }
  818. return fetchFail(er)
  819. }
  820. const fetchFail = er => {
  821. const { aborted } = ac.signal
  822. const allowStaleAborted =
  823. aborted && options.allowStaleOnFetchAbort
  824. const allowStale =
  825. allowStaleAborted || options.allowStaleOnFetchRejection
  826. const noDelete = allowStale || options.noDeleteOnFetchRejection
  827. if (this.valList[index] === p) {
  828. // if we allow stale on fetch rejections, then we need to ensure that
  829. // the stale value is not removed from the cache when the fetch fails.
  830. const del = !noDelete || p.__staleWhileFetching === undefined
  831. if (del) {
  832. this.delete(k)
  833. } else if (!allowStaleAborted) {
  834. // still replace the *promise* with the stale value,
  835. // since we are done with the promise at this point.
  836. // leave it untouched if we're still waiting for an
  837. // aborted background fetch that hasn't yet returned.
  838. this.valList[index] = p.__staleWhileFetching
  839. }
  840. }
  841. if (allowStale) {
  842. if (options.status && p.__staleWhileFetching !== undefined) {
  843. options.status.returnedStale = true
  844. }
  845. return p.__staleWhileFetching
  846. } else if (p.__returned === p) {
  847. throw er
  848. }
  849. }
  850. const pcall = (res, rej) => {
  851. this.fetchMethod(k, v, fetchOpts).then(v => res(v), rej)
  852. // ignored, we go until we finish, regardless.
  853. // defer check until we are actually aborting,
  854. // so fetchMethod can override.
  855. ac.signal.addEventListener('abort', () => {
  856. if (
  857. !options.ignoreFetchAbort ||
  858. options.allowStaleOnFetchAbort
  859. ) {
  860. res()
  861. // when it eventually resolves, update the cache.
  862. if (options.allowStaleOnFetchAbort) {
  863. res = v => cb(v, true)
  864. }
  865. }
  866. })
  867. }
  868. if (options.status) options.status.fetchDispatched = true
  869. const p = new Promise(pcall).then(cb, eb)
  870. p.__abortController = ac
  871. p.__staleWhileFetching = v
  872. p.__returned = null
  873. if (index === undefined) {
  874. // internal, don't expose status.
  875. this.set(k, p, { ...fetchOpts.options, status: undefined })
  876. index = this.keyMap.get(k)
  877. } else {
  878. this.valList[index] = p
  879. }
  880. return p
  881. }
  882. isBackgroundFetch(p) {
  883. return (
  884. p &&
  885. typeof p === 'object' &&
  886. typeof p.then === 'function' &&
  887. Object.prototype.hasOwnProperty.call(
  888. p,
  889. '__staleWhileFetching'
  890. ) &&
  891. Object.prototype.hasOwnProperty.call(p, '__returned') &&
  892. (p.__returned === p || p.__returned === null)
  893. )
  894. }
  895. // this takes the union of get() and set() opts, because it does both
  896. async fetch(
  897. k,
  898. {
  899. // get options
  900. allowStale = this.allowStale,
  901. updateAgeOnGet = this.updateAgeOnGet,
  902. noDeleteOnStaleGet = this.noDeleteOnStaleGet,
  903. // set options
  904. ttl = this.ttl,
  905. noDisposeOnSet = this.noDisposeOnSet,
  906. size = 0,
  907. sizeCalculation = this.sizeCalculation,
  908. noUpdateTTL = this.noUpdateTTL,
  909. // fetch exclusive options
  910. noDeleteOnFetchRejection = this.noDeleteOnFetchRejection,
  911. allowStaleOnFetchRejection = this.allowStaleOnFetchRejection,
  912. ignoreFetchAbort = this.ignoreFetchAbort,
  913. allowStaleOnFetchAbort = this.allowStaleOnFetchAbort,
  914. fetchContext = this.fetchContext,
  915. forceRefresh = false,
  916. status,
  917. signal,
  918. } = {}
  919. ) {
  920. if (!this.fetchMethod) {
  921. if (status) status.fetch = 'get'
  922. return this.get(k, {
  923. allowStale,
  924. updateAgeOnGet,
  925. noDeleteOnStaleGet,
  926. status,
  927. })
  928. }
  929. const options = {
  930. allowStale,
  931. updateAgeOnGet,
  932. noDeleteOnStaleGet,
  933. ttl,
  934. noDisposeOnSet,
  935. size,
  936. sizeCalculation,
  937. noUpdateTTL,
  938. noDeleteOnFetchRejection,
  939. allowStaleOnFetchRejection,
  940. allowStaleOnFetchAbort,
  941. ignoreFetchAbort,
  942. status,
  943. signal,
  944. }
  945. let index = this.keyMap.get(k)
  946. if (index === undefined) {
  947. if (status) status.fetch = 'miss'
  948. const p = this.backgroundFetch(k, index, options, fetchContext)
  949. return (p.__returned = p)
  950. } else {
  951. // in cache, maybe already fetching
  952. const v = this.valList[index]
  953. if (this.isBackgroundFetch(v)) {
  954. const stale =
  955. allowStale && v.__staleWhileFetching !== undefined
  956. if (status) {
  957. status.fetch = 'inflight'
  958. if (stale) status.returnedStale = true
  959. }
  960. return stale ? v.__staleWhileFetching : (v.__returned = v)
  961. }
  962. // if we force a refresh, that means do NOT serve the cached value,
  963. // unless we are already in the process of refreshing the cache.
  964. const isStale = this.isStale(index)
  965. if (!forceRefresh && !isStale) {
  966. if (status) status.fetch = 'hit'
  967. this.moveToTail(index)
  968. if (updateAgeOnGet) {
  969. this.updateItemAge(index)
  970. }
  971. this.statusTTL(status, index)
  972. return v
  973. }
  974. // ok, it is stale or a forced refresh, and not already fetching.
  975. // refresh the cache.
  976. const p = this.backgroundFetch(k, index, options, fetchContext)
  977. const hasStale = p.__staleWhileFetching !== undefined
  978. const staleVal = hasStale && allowStale
  979. if (status) {
  980. status.fetch = hasStale && isStale ? 'stale' : 'refresh'
  981. if (staleVal && isStale) status.returnedStale = true
  982. }
  983. return staleVal ? p.__staleWhileFetching : (p.__returned = p)
  984. }
  985. }
  986. get(
  987. k,
  988. {
  989. allowStale = this.allowStale,
  990. updateAgeOnGet = this.updateAgeOnGet,
  991. noDeleteOnStaleGet = this.noDeleteOnStaleGet,
  992. status,
  993. } = {}
  994. ) {
  995. const index = this.keyMap.get(k)
  996. if (index !== undefined) {
  997. const value = this.valList[index]
  998. const fetching = this.isBackgroundFetch(value)
  999. this.statusTTL(status, index)
  1000. if (this.isStale(index)) {
  1001. if (status) status.get = 'stale'
  1002. // delete only if not an in-flight background fetch
  1003. if (!fetching) {
  1004. if (!noDeleteOnStaleGet) {
  1005. this.delete(k)
  1006. }
  1007. if (status) status.returnedStale = allowStale
  1008. return allowStale ? value : undefined
  1009. } else {
  1010. if (status) {
  1011. status.returnedStale =
  1012. allowStale && value.__staleWhileFetching !== undefined
  1013. }
  1014. return allowStale ? value.__staleWhileFetching : undefined
  1015. }
  1016. } else {
  1017. if (status) status.get = 'hit'
  1018. // if we're currently fetching it, we don't actually have it yet
  1019. // it's not stale, which means this isn't a staleWhileRefetching.
  1020. // If it's not stale, and fetching, AND has a __staleWhileFetching
  1021. // value, then that means the user fetched with {forceRefresh:true},
  1022. // so it's safe to return that value.
  1023. if (fetching) {
  1024. return value.__staleWhileFetching
  1025. }
  1026. this.moveToTail(index)
  1027. if (updateAgeOnGet) {
  1028. this.updateItemAge(index)
  1029. }
  1030. return value
  1031. }
  1032. } else if (status) {
  1033. status.get = 'miss'
  1034. }
  1035. }
  1036. connect(p, n) {
  1037. this.prev[n] = p
  1038. this.next[p] = n
  1039. }
  1040. moveToTail(index) {
  1041. // if tail already, nothing to do
  1042. // if head, move head to next[index]
  1043. // else
  1044. // move next[prev[index]] to next[index] (head has no prev)
  1045. // move prev[next[index]] to prev[index]
  1046. // prev[index] = tail
  1047. // next[tail] = index
  1048. // tail = index
  1049. if (index !== this.tail) {
  1050. if (index === this.head) {
  1051. this.head = this.next[index]
  1052. } else {
  1053. this.connect(this.prev[index], this.next[index])
  1054. }
  1055. this.connect(this.tail, index)
  1056. this.tail = index
  1057. }
  1058. }
  1059. get del() {
  1060. deprecatedMethod('del', 'delete')
  1061. return this.delete
  1062. }
  1063. delete(k) {
  1064. let deleted = false
  1065. if (this.size !== 0) {
  1066. const index = this.keyMap.get(k)
  1067. if (index !== undefined) {
  1068. deleted = true
  1069. if (this.size === 1) {
  1070. this.clear()
  1071. } else {
  1072. this.removeItemSize(index)
  1073. const v = this.valList[index]
  1074. if (this.isBackgroundFetch(v)) {
  1075. v.__abortController.abort(new Error('deleted'))
  1076. } else {
  1077. this.dispose(v, k, 'delete')
  1078. if (this.disposeAfter) {
  1079. this.disposed.push([v, k, 'delete'])
  1080. }
  1081. }
  1082. this.keyMap.delete(k)
  1083. this.keyList[index] = null
  1084. this.valList[index] = null
  1085. if (index === this.tail) {
  1086. this.tail = this.prev[index]
  1087. } else if (index === this.head) {
  1088. this.head = this.next[index]
  1089. } else {
  1090. this.next[this.prev[index]] = this.next[index]
  1091. this.prev[this.next[index]] = this.prev[index]
  1092. }
  1093. this.size--
  1094. this.free.push(index)
  1095. }
  1096. }
  1097. }
  1098. if (this.disposed) {
  1099. while (this.disposed.length) {
  1100. this.disposeAfter(...this.disposed.shift())
  1101. }
  1102. }
  1103. return deleted
  1104. }
  1105. clear() {
  1106. for (const index of this.rindexes({ allowStale: true })) {
  1107. const v = this.valList[index]
  1108. if (this.isBackgroundFetch(v)) {
  1109. v.__abortController.abort(new Error('deleted'))
  1110. } else {
  1111. const k = this.keyList[index]
  1112. this.dispose(v, k, 'delete')
  1113. if (this.disposeAfter) {
  1114. this.disposed.push([v, k, 'delete'])
  1115. }
  1116. }
  1117. }
  1118. this.keyMap.clear()
  1119. this.valList.fill(null)
  1120. this.keyList.fill(null)
  1121. if (this.ttls) {
  1122. this.ttls.fill(0)
  1123. this.starts.fill(0)
  1124. }
  1125. if (this.sizes) {
  1126. this.sizes.fill(0)
  1127. }
  1128. this.head = 0
  1129. this.tail = 0
  1130. this.initialFill = 1
  1131. this.free.length = 0
  1132. this.calculatedSize = 0
  1133. this.size = 0
  1134. if (this.disposed) {
  1135. while (this.disposed.length) {
  1136. this.disposeAfter(...this.disposed.shift())
  1137. }
  1138. }
  1139. }
  1140. get reset() {
  1141. deprecatedMethod('reset', 'clear')
  1142. return this.clear
  1143. }
  1144. get length() {
  1145. deprecatedProperty('length', 'size')
  1146. return this.size
  1147. }
  1148. static get AbortController() {
  1149. return AC
  1150. }
  1151. static get AbortSignal() {
  1152. return AS
  1153. }
  1154. }
  1155. export default LRUCache