scoped-cache.ts 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. type ScopedCacheOptions<T> = {
  2. maxEntries?: number
  3. ttlMs?: number
  4. dispose?: (value: T, key: string) => void
  5. now?: () => number
  6. }
  7. type Entry<T> = {
  8. value: T
  9. touchedAt: number
  10. }
  11. export function createScopedCache<T>(createValue: (key: string) => T, options: ScopedCacheOptions<T> = {}) {
  12. const store = new Map<string, Entry<T>>()
  13. const now = options.now ?? Date.now
  14. const dispose = (key: string, entry: Entry<T>) => {
  15. options.dispose?.(entry.value, key)
  16. }
  17. const expired = (entry: Entry<T>) => {
  18. if (options.ttlMs === undefined) return false
  19. return now() - entry.touchedAt >= options.ttlMs
  20. }
  21. const sweep = () => {
  22. if (options.ttlMs === undefined) return
  23. for (const [key, entry] of store) {
  24. if (!expired(entry)) continue
  25. store.delete(key)
  26. dispose(key, entry)
  27. }
  28. }
  29. const touch = (key: string, entry: Entry<T>) => {
  30. entry.touchedAt = now()
  31. store.delete(key)
  32. store.set(key, entry)
  33. }
  34. const prune = () => {
  35. if (options.maxEntries === undefined) return
  36. while (store.size > options.maxEntries) {
  37. const key = store.keys().next().value
  38. if (!key) return
  39. const entry = store.get(key)
  40. store.delete(key)
  41. if (!entry) continue
  42. dispose(key, entry)
  43. }
  44. }
  45. const remove = (key: string) => {
  46. const entry = store.get(key)
  47. if (!entry) return
  48. store.delete(key)
  49. dispose(key, entry)
  50. return entry.value
  51. }
  52. const peek = (key: string) => {
  53. sweep()
  54. const entry = store.get(key)
  55. if (!entry) return
  56. if (!expired(entry)) return entry.value
  57. store.delete(key)
  58. dispose(key, entry)
  59. }
  60. const get = (key: string) => {
  61. sweep()
  62. const entry = store.get(key)
  63. if (entry && !expired(entry)) {
  64. touch(key, entry)
  65. return entry.value
  66. }
  67. if (entry) {
  68. store.delete(key)
  69. dispose(key, entry)
  70. }
  71. const created = {
  72. value: createValue(key),
  73. touchedAt: now(),
  74. }
  75. store.set(key, created)
  76. prune()
  77. return created.value
  78. }
  79. const clear = () => {
  80. for (const [key, entry] of store) {
  81. dispose(key, entry)
  82. }
  83. store.clear()
  84. }
  85. return {
  86. get,
  87. peek,
  88. delete: remove,
  89. clear,
  90. }
  91. }