hanzio 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +786 -239
  2. package/dist/api-wrapper/builders.d.ts +4 -3
  3. package/dist/api-wrapper/create-client.d.ts +1 -1
  4. package/dist/api-wrapper/describe.d.ts +16 -0
  5. package/dist/api-wrapper/endpoint.d.ts +18 -0
  6. package/dist/api-wrapper/errors.d.ts +39 -2
  7. package/dist/api-wrapper/index.d.ts +5 -0
  8. package/dist/api-wrapper/otel.d.ts +44 -0
  9. package/dist/api-wrapper/paginate.d.ts +12 -0
  10. package/dist/api-wrapper/redaction.d.ts +2 -0
  11. package/dist/api-wrapper/shared.d.ts +16 -2
  12. package/dist/api-wrapper/testing.d.ts +23 -0
  13. package/dist/api-wrapper/transport.d.ts +4 -5
  14. package/dist/api-wrapper/types.d.ts +103 -18
  15. package/dist/array/index.d.ts +50 -4
  16. package/dist/cache/createCache.d.ts +31 -0
  17. package/dist/cache/index.d.ts +19 -2
  18. package/dist/color/contrast.d.ts +40 -0
  19. package/dist/color/index.d.ts +2 -0
  20. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  21. package/dist/cool-console-log/index.d.ts +34 -5
  22. package/dist/crypto/encoding.d.ts +18 -0
  23. package/dist/crypto/hash.d.ts +33 -0
  24. package/dist/crypto/index.d.ts +4 -0
  25. package/dist/crypto/sealer.d.ts +25 -0
  26. package/dist/crypto/webhook.d.ts +20 -0
  27. package/dist/date/index.d.ts +46 -2
  28. package/dist/emitter/index.d.ts +21 -0
  29. package/dist/error/index.d.ts +12 -0
  30. package/dist/format/index.d.ts +102 -0
  31. package/dist/function/index.d.ts +60 -0
  32. package/dist/guard/index.d.ts +20 -0
  33. package/dist/i18n/index.d.ts +37 -0
  34. package/dist/index-0zvtcbzy.js +224 -0
  35. package/dist/index-181e6vfw.js +29 -0
  36. package/dist/index-8r2vdzjg.js +87 -0
  37. package/dist/index-hy7kbb99.js +157 -0
  38. package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
  39. package/dist/index-k9yx6bne.js +89 -0
  40. package/dist/index-kj1phgrx.js +169 -0
  41. package/dist/index-mnkcfyx8.js +78 -0
  42. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  43. package/dist/index-rp5pqghz.js +157 -0
  44. package/dist/index-srt06xd2.js +15 -0
  45. package/dist/index-wjyb9bsd.js +263 -0
  46. package/dist/index.d.ts +7 -7
  47. package/dist/jwt/index.d.ts +50 -2
  48. package/dist/math/index.d.ts +2 -2
  49. package/dist/math/linearRegressionTrend.d.ts +14 -1
  50. package/dist/math/scale.d.ts +8 -0
  51. package/dist/object/index.d.ts +19 -0
  52. package/dist/p-queue/index.d.ts +1 -1
  53. package/dist/promise/index.d.ts +97 -2
  54. package/dist/sitemap/index.d.ts +48 -6
  55. package/dist/src/api-wrapper/index.js +60 -1130
  56. package/dist/src/api-wrapper/testing.js +20 -0
  57. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  58. package/dist/src/crypto/index.js +195 -0
  59. package/dist/src/format/index.js +211 -0
  60. package/dist/src/i18n/index.js +36 -0
  61. package/dist/src/index.js +739 -3145
  62. package/dist/src/jwt/index.js +138 -92
  63. package/dist/src/p-queue/index.js +9 -437
  64. package/dist/src/secrets/index.js +44 -1428
  65. package/dist/src/secrets/vite.js +6 -39
  66. package/dist/src/secrets/worker.js +10 -273
  67. package/dist/src/sitemap/index.js +183 -511
  68. package/dist/src/storage/index.js +119 -0
  69. package/dist/src/zod/index.js +340 -167
  70. package/dist/state/index.d.ts +21 -0
  71. package/dist/storage/index.d.ts +58 -0
  72. package/dist/string/index.d.ts +75 -2
  73. package/dist/typedswitch/index.d.ts +31 -24
  74. package/dist/types/index.d.ts +11 -0
  75. package/dist/url/index.d.ts +50 -1
  76. package/dist/zod/index.d.ts +32 -3
  77. package/dist/zod/introspect.d.ts +19 -0
  78. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  79. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  80. package/dist/zod/zodToTypeString.d.ts +8 -3
  81. package/package.json +78 -7
  82. package/src/api-wrapper/actions-impl.ts +11 -72
  83. package/src/api-wrapper/builders.ts +32 -25
  84. package/src/api-wrapper/create-client.ts +171 -41
  85. package/src/api-wrapper/describe.ts +157 -0
  86. package/src/api-wrapper/endpoint.ts +26 -0
  87. package/src/api-wrapper/error-reporting.ts +7 -1
  88. package/src/api-wrapper/errors.ts +107 -2
  89. package/src/api-wrapper/headers.ts +11 -6
  90. package/src/api-wrapper/index.ts +5 -0
  91. package/src/api-wrapper/otel.ts +139 -0
  92. package/src/api-wrapper/paginate.ts +28 -0
  93. package/src/api-wrapper/redaction.ts +42 -0
  94. package/src/api-wrapper/shared.ts +21 -2
  95. package/src/api-wrapper/testing.ts +48 -0
  96. package/src/api-wrapper/transport.ts +41 -43
  97. package/src/api-wrapper/types.ts +185 -18
  98. package/src/array/index.ts +195 -42
  99. package/src/cache/createCache.ts +157 -0
  100. package/src/cache/index.ts +132 -147
  101. package/src/color/contrast.ts +163 -0
  102. package/src/color/index.ts +22 -0
  103. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  104. package/src/cool-console-log/index.ts +128 -41
  105. package/src/crypto/encoding.ts +83 -0
  106. package/src/crypto/hash.ts +170 -0
  107. package/src/crypto/index.ts +26 -0
  108. package/src/crypto/sealer.ts +168 -0
  109. package/src/crypto/webhook.ts +86 -0
  110. package/src/date/index.ts +109 -34
  111. package/src/emitter/index.ts +82 -0
  112. package/src/error/index.ts +63 -0
  113. package/src/format/index.ts +389 -0
  114. package/src/function/index.ts +201 -0
  115. package/src/guard/index.ts +80 -0
  116. package/src/i18n/index.ts +125 -0
  117. package/src/index.ts +11 -7
  118. package/src/jwt/index.ts +199 -125
  119. package/src/math/index.ts +5 -10
  120. package/src/math/linearRegressionTrend.ts +33 -8
  121. package/src/math/scale.ts +17 -0
  122. package/src/object/index.ts +99 -0
  123. package/src/p-queue/index.ts +2 -0
  124. package/src/promise/index.ts +304 -9
  125. package/src/sitemap/index.ts +293 -130
  126. package/src/state/index.ts +133 -81
  127. package/src/storage/index.ts +187 -0
  128. package/src/string/index.ts +261 -23
  129. package/src/typedswitch/index.ts +58 -38
  130. package/src/types/index.ts +14 -0
  131. package/src/url/index.ts +146 -1
  132. package/src/zod/index.ts +105 -9
  133. package/src/zod/introspect.ts +103 -0
  134. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  135. package/src/zod/zodMinMaxFilter.ts +37 -13
  136. package/src/zod/zodToTypeString.ts +184 -181
  137. package/dist/api-wrapper/index.js +0 -250
  138. package/dist/gzip/index.d.ts +0 -2
  139. package/dist/gzip/index.js +0 -16
  140. package/dist/index.js +0 -1823
  141. package/dist/jwt/index.js +0 -19
  142. package/dist/sitemap/index.js +0 -552
  143. package/dist/src/gzip/index.js +0 -16
  144. package/dist/src/secrets/loaders.js +0 -1529
  145. package/dist/zod/index.js +0 -210
@@ -1,9 +1,20 @@
1
+ import { stableStringify } from '../string'
2
+
1
3
  export interface ArrayDifference<TCurrent, TNext> {
2
4
  new: TNext[]
5
+ /** Items present in both arrays (`update` + `unchanged`). */
3
6
  upsert: TNext[]
4
7
  delete: TCurrent[]
5
8
  }
6
9
 
10
+ export interface ArrayDifferenceWithChanges<TCurrent, TNext>
11
+ extends ArrayDifference<TCurrent, TNext> {
12
+ /** Matched items where `isEqual` returned false. */
13
+ update: TNext[]
14
+ /** Matched items where `isEqual` returned true. */
15
+ unchanged: TNext[]
16
+ }
17
+
7
18
  type PropertyKeyValue = string | number | symbol
8
19
  type NonNullableValue<T> = T extends null | undefined ? never : T
9
20
  type ItemsMatchingKeyValue<T, TKey extends keyof T, TValue> = [
@@ -25,51 +36,79 @@ export function chunkArray<T>(array: readonly T[], chunkSize: number): T[][] {
25
36
  return chunks
26
37
  }
27
38
 
28
- export function findArrayDifferenceByKey<
29
- TCurrent,
30
- TNext,
31
- TKey extends keyof TCurrent & keyof TNext
32
- >(
39
+ type DifferenceKey<TCurrent, TNext> =
40
+ | (keyof TCurrent & keyof TNext)
41
+ | ((item: TCurrent | TNext) => unknown)
42
+
43
+ /**
44
+ * Splits `next` into items that are new or also in `current` (`upsert`), and
45
+ * lists the `current` items missing from `next` (`delete`). Items match by a
46
+ * shared key name or a key function; key values are compared as strings.
47
+ * Pass `isEqual` to also split `upsert` into `update` and `unchanged`.
48
+ */
49
+ export function findArrayDifferenceByKey<TCurrent, TNext>(
33
50
  current: readonly TCurrent[],
34
51
  next: readonly TNext[],
35
- key: TKey
36
- ): ArrayDifference<TCurrent, TNext> {
37
- const currentByKey = new Map(
38
- current.map((item) => [String(item[key]), item] as const)
39
- )
40
- const itemsToDelete = [...current]
52
+ key: DifferenceKey<TCurrent, TNext>,
53
+ options: { isEqual: (current: TCurrent, next: TNext) => boolean }
54
+ ): ArrayDifferenceWithChanges<TCurrent, TNext>
55
+ export function findArrayDifferenceByKey<TCurrent, TNext>(
56
+ current: readonly TCurrent[],
57
+ next: readonly TNext[],
58
+ key: DifferenceKey<TCurrent, TNext>,
59
+ options?: { isEqual?: (current: TCurrent, next: TNext) => boolean }
60
+ ): ArrayDifference<TCurrent, TNext>
61
+ export function findArrayDifferenceByKey<TCurrent, TNext>(
62
+ current: readonly TCurrent[],
63
+ next: readonly TNext[],
64
+ key: DifferenceKey<TCurrent, TNext>,
65
+ options: { isEqual?: (current: TCurrent, next: TNext) => boolean } = {}
66
+ ):
67
+ | ArrayDifference<TCurrent, TNext>
68
+ | ArrayDifferenceWithChanges<TCurrent, TNext> {
69
+ const { isEqual } = options
70
+ const getKey = (item: TCurrent | TNext) =>
71
+ String(typeof key === 'function' ? key(item) : item[key])
72
+ const currentByKey = new Map(current.map((item) => [getKey(item), item]))
73
+ const matchedKeys = new Set<string>()
41
74
  const newItems: TNext[] = []
42
75
  const upsertItems: TNext[] = []
76
+ const updateItems: TNext[] = []
77
+ const unchangedItems: TNext[] = []
43
78
 
44
79
  for (const item of next) {
45
- const keyValue = String(item[key])
46
-
47
- if (!currentByKey.has(keyValue)) {
80
+ const itemKey = getKey(item)
81
+ if (!currentByKey.has(itemKey)) {
48
82
  newItems.push(item)
49
83
  continue
50
84
  }
51
85
 
86
+ matchedKeys.add(itemKey)
52
87
  upsertItems.push(item)
53
- const deleteIndex = itemsToDelete.findIndex(
54
- (currentItem) => String(currentItem[key]) === keyValue
55
- )
56
- if (deleteIndex >= 0) {
57
- itemsToDelete.splice(deleteIndex, 1)
88
+ if (isEqual) {
89
+ const target = isEqual(currentByKey.get(itemKey) as TCurrent, item)
90
+ ? unchangedItems
91
+ : updateItems
92
+ target.push(item)
58
93
  }
59
94
  }
60
95
 
61
- return {
96
+ const difference = {
62
97
  new: newItems,
63
98
  upsert: upsertItems,
64
- delete: itemsToDelete
99
+ delete: current.filter((item) => !matchedKeys.has(getKey(item)))
65
100
  }
101
+
102
+ return isEqual
103
+ ? { ...difference, update: updateItems, unchanged: unchangedItems }
104
+ : difference
66
105
  }
67
106
 
68
107
  export function getUniqueValues<T>(array: readonly T[]): T[] {
69
108
  const seen = new Map<string, T>()
70
109
 
71
110
  for (const element of array) {
72
- const key = JSON.stringify(element)
111
+ const key = stableStringify(element)
73
112
  if (!seen.has(key)) {
74
113
  seen.set(key, element)
75
114
  }
@@ -82,15 +121,15 @@ export function compact<T>(array: readonly T[]): NonNullableValue<T>[] {
82
121
  return array.filter((item): item is NonNullableValue<T> => item != null)
83
122
  }
84
123
 
85
- export function getUniqueValuesByKey<T, TKey extends keyof T>(
124
+ export function getUniqueValuesByKey<T>(
86
125
  array: readonly T[],
87
- key: TKey
126
+ keyOrFn: keyof T | ((item: T) => unknown)
88
127
  ): T[] {
89
- const seen = new Set<T[TKey]>()
128
+ const seen = new Set<unknown>()
90
129
  const result: T[] = []
91
130
 
92
131
  for (const item of array) {
93
- const value = item[key]
132
+ const value = typeof keyOrFn === 'function' ? keyOrFn(item) : item[keyOrFn]
94
133
  if (seen.has(value)) continue
95
134
  seen.add(value)
96
135
  result.push(item)
@@ -103,28 +142,53 @@ export function groupBy<
103
142
  T,
104
143
  TKey extends keyof T,
105
144
  TGroupKey extends Extract<T[TKey], PropertyKeyValue>
106
- >(array: readonly T[], key: TKey): Record<TGroupKey, T[]>
145
+ >(array: readonly T[], key: TKey): Partial<Record<TGroupKey, T[]>>
107
146
  export function groupBy<T, TGroupKey extends PropertyKeyValue>(
108
147
  array: readonly T[],
109
148
  keyFn: (item: T) => TGroupKey
110
- ): Record<TGroupKey, T[]>
149
+ ): Partial<Record<TGroupKey, T[]>>
111
150
  export function groupBy<T, TGroupKey extends PropertyKeyValue>(
112
151
  array: readonly T[],
113
152
  keyOrFn: keyof T | ((item: T) => TGroupKey)
114
- ): Record<TGroupKey, T[]> {
115
- return array.reduce(
116
- (result, item) => {
117
- const groupKey =
118
- typeof keyOrFn === 'function'
119
- ? keyOrFn(item)
120
- : (item[keyOrFn] as TGroupKey)
121
-
122
- result[groupKey] ??= []
123
- result[groupKey].push(item)
124
- return result
125
- },
126
- {} as Record<TGroupKey, T[]>
127
- )
153
+ ): Partial<Record<TGroupKey, T[]>> {
154
+ const result: Partial<Record<TGroupKey, T[]>> = {}
155
+
156
+ for (const item of array) {
157
+ const groupKey =
158
+ typeof keyOrFn === 'function'
159
+ ? keyOrFn(item)
160
+ : (item[keyOrFn] as TGroupKey)
161
+ result[groupKey] ??= []
162
+ result[groupKey].push(item)
163
+ }
164
+
165
+ return result
166
+ }
167
+
168
+ export function countBy<
169
+ T,
170
+ TKey extends keyof T,
171
+ TGroupKey extends Extract<T[TKey], PropertyKeyValue>
172
+ >(array: readonly T[], key: TKey): Partial<Record<TGroupKey, number>>
173
+ export function countBy<T, TGroupKey extends PropertyKeyValue>(
174
+ array: readonly T[],
175
+ keyFn: (item: T) => TGroupKey
176
+ ): Partial<Record<TGroupKey, number>>
177
+ export function countBy<T, TGroupKey extends PropertyKeyValue>(
178
+ array: readonly T[],
179
+ keyOrFn: keyof T | ((item: T) => TGroupKey)
180
+ ): Partial<Record<TGroupKey, number>> {
181
+ const result: Partial<Record<TGroupKey, number>> = {}
182
+
183
+ for (const item of array) {
184
+ const groupKey =
185
+ typeof keyOrFn === 'function'
186
+ ? keyOrFn(item)
187
+ : (item[keyOrFn] as TGroupKey)
188
+ result[groupKey] = (result[groupKey] ?? 0) + 1
189
+ }
190
+
191
+ return result
128
192
  }
129
193
 
130
194
  export function keyBy<
@@ -185,3 +249,92 @@ export function pickItemsInArray<
185
249
  values.includes(item[key])
186
250
  )
187
251
  }
252
+
253
+ /** Wraps a single value in an array; `null`/`undefined` become `[]`. Arrays are copied. */
254
+ export function toArray<T>(value: T | readonly T[] | null | undefined): T[] {
255
+ if (value == null) return []
256
+ if (Array.isArray(value)) return [...(value as readonly T[])]
257
+ return [value as T]
258
+ }
259
+
260
+ type NumericKey<T> = {
261
+ [K in keyof T]: T[K] extends number | null | undefined ? K : never
262
+ }[keyof T]
263
+
264
+ /** Sums a numeric property or callback result; nullish values count as 0. */
265
+ export function sumBy<T>(
266
+ array: readonly T[],
267
+ fn: (item: T) => number | null | undefined
268
+ ): number
269
+ export function sumBy<T>(array: readonly T[], key: NumericKey<T>): number
270
+ export function sumBy<T>(
271
+ array: readonly T[],
272
+ keyOrFn: NumericKey<T> | ((item: T) => number | null | undefined)
273
+ ): number {
274
+ let sum = 0
275
+ for (const item of array) {
276
+ const value =
277
+ typeof keyOrFn === 'function'
278
+ ? keyOrFn(item)
279
+ : (item[keyOrFn] as number | null | undefined)
280
+ sum += value ?? 0
281
+ }
282
+ return sum
283
+ }
284
+
285
+ type SortSelector<T> = keyof T | ((item: T) => unknown)
286
+ export type SortCriterion<T> =
287
+ | SortSelector<T>
288
+ | { by: SortSelector<T>; order: 'asc' | 'desc' }
289
+
290
+ /**
291
+ * Returns a sorted copy. Each criterion is a key, a callback, or
292
+ * `{ by, order }`; later criteria break ties. The sort is stable, and
293
+ * `null`/`undefined` values always go last. Without criteria the items
294
+ * themselves are compared.
295
+ */
296
+ export function sortBy<T>(
297
+ array: readonly T[],
298
+ ...criteria: SortCriterion<T>[]
299
+ ): T[] {
300
+ const comparators = (criteria.length ? criteria : [(item: T) => item]).map(
301
+ (criterion) => {
302
+ const { by, order } =
303
+ typeof criterion === 'object'
304
+ ? criterion
305
+ : { by: criterion, order: 'asc' as const }
306
+ const select = (item: T) =>
307
+ typeof by === 'function' ? by(item) : item[by as keyof T]
308
+ const direction = order === 'desc' ? -1 : 1
309
+
310
+ return (left: T, right: T) => {
311
+ const a = select(left) as number
312
+ const b = select(right) as number
313
+ if (a == null || b == null) return a == null ? (b == null ? 0 : 1) : -1
314
+ return a < b ? -direction : a > b ? direction : 0
315
+ }
316
+ }
317
+ )
318
+
319
+ return [...array].sort((left, right) => {
320
+ for (const compare of comparators) {
321
+ const result = compare(left, right)
322
+ if (result !== 0) return result
323
+ }
324
+ return 0
325
+ })
326
+ }
327
+
328
+ /**
329
+ * Numbers from `start` up to (not including) `end`. With one argument it
330
+ * counts from 0. `step` defaults to 1, or -1 when `end < start`.
331
+ */
332
+ export function range(start: number, end?: number, step?: number): number[] {
333
+ const from = end === undefined ? 0 : start
334
+ const to = end ?? start
335
+ const increment = step ?? (to < from ? -1 : 1)
336
+ if (increment === 0) throw new Error('Step must not be zero')
337
+
338
+ const length = Math.max(Math.ceil((to - from) / increment), 0)
339
+ return Array.from({ length }, (_, index) => from + index * increment)
340
+ }
@@ -0,0 +1,157 @@
1
+ type Entry = {
2
+ value?: unknown
3
+ hasValue: boolean
4
+ expiresAt?: number
5
+ /** `expiresAt - staleMarginMs`: getOrSet refreshes from here on. */
6
+ refreshAt?: number
7
+ inflight?: Promise<unknown>
8
+ }
9
+
10
+ export type CacheSetOptions<T = unknown> = {
11
+ /** Lifetime of the entry; a function derives it from the value (e.g. a token's `expires_in`). */
12
+ ttlMs?: number | ((value: T) => number)
13
+ /** getOrSet recomputes this long before expiry; get still returns the entry until it expires. */
14
+ staleMarginMs?: number
15
+ }
16
+
17
+ export type CreateCacheOptions = {
18
+ /** TTL used when a call doesn't pass `ttlMs` (default: no expiry). */
19
+ defaultTtlMs?: number
20
+ /** `staleMarginMs` used when a call doesn't pass one (default: 0). */
21
+ staleMarginMs?: number
22
+ /** Evicts the least recently used entry beyond this size (default: unbounded). */
23
+ maxEntries?: number
24
+ /** Clock, for tests (default: `Date.now`). */
25
+ now?: () => number
26
+ }
27
+
28
+ export type KeyValueCache = {
29
+ /**
30
+ * Get-or-compute. Concurrent callers share one in-flight call; rejections
31
+ * aren't cached (a stale value stays readable until it expires).
32
+ */
33
+ getOrSet<T>(
34
+ key: string,
35
+ fn: () => Promise<T> | T,
36
+ options?: CacheSetOptions<T>
37
+ ): Promise<T>
38
+ get<T>(key: string): T | undefined
39
+ has(key: string): boolean
40
+ set<T>(key: string, value: T, options?: CacheSetOptions<T>): void
41
+ delete(key: string): void
42
+ clear(): void
43
+ readonly size: number
44
+ }
45
+
46
+ /** A small in-memory key/value cache with TTL, LRU bound and in-flight dedupe. */
47
+ export function createCache(options: CreateCacheOptions = {}): KeyValueCache {
48
+ const {
49
+ defaultTtlMs,
50
+ staleMarginMs: defaultStaleMarginMs = 0,
51
+ maxEntries,
52
+ now = Date.now
53
+ } = options
54
+ const store = new Map<string, Entry>()
55
+
56
+ // Stores `value` on `entry` with an expiry counted from `startedAt`.
57
+ const fill = <T>(
58
+ entry: Entry,
59
+ value: T,
60
+ startedAt: number,
61
+ setOptions: CacheSetOptions<T> = {}
62
+ ) => {
63
+ const { ttlMs = defaultTtlMs, staleMarginMs = defaultStaleMarginMs } =
64
+ setOptions
65
+ const ttl = typeof ttlMs === 'function' ? ttlMs(value) : ttlMs
66
+ entry.value = value
67
+ entry.hasValue = true
68
+ entry.expiresAt = ttl === undefined ? undefined : startedAt + ttl
69
+ entry.refreshAt =
70
+ entry.expiresAt === undefined
71
+ ? undefined
72
+ : entry.expiresAt - staleMarginMs
73
+ return entry
74
+ }
75
+
76
+ const isExpired = (entry: Entry) =>
77
+ entry.expiresAt !== undefined && entry.expiresAt <= now()
78
+
79
+ const hasFreshValue = (entry: Entry | undefined) =>
80
+ entry?.hasValue === true && !isExpired(entry)
81
+
82
+ // Map keeps insertion order, so re-inserting on access makes the first key
83
+ // the least recently used.
84
+ const touch = (key: string, entry: Entry) => {
85
+ store.delete(key)
86
+ store.set(key, entry)
87
+ if (maxEntries !== undefined) {
88
+ while (store.size > maxEntries) {
89
+ store.delete(store.keys().next().value as string)
90
+ }
91
+ }
92
+ }
93
+
94
+ const read = (key: string): Entry | undefined => {
95
+ const entry = store.get(key)
96
+ if (!entry) return undefined
97
+ if (isExpired(entry) && !entry.inflight) {
98
+ store.delete(key)
99
+ return undefined
100
+ }
101
+ touch(key, entry)
102
+ return entry
103
+ }
104
+
105
+ return {
106
+ async getOrSet<T>(
107
+ key: string,
108
+ fn: () => Promise<T> | T,
109
+ setOptions?: CacheSetOptions<T>
110
+ ): Promise<T> {
111
+ const existing = read(key)
112
+ if (existing?.inflight) return existing.inflight as Promise<T>
113
+ const isStale =
114
+ existing?.refreshAt !== undefined && existing.refreshAt <= now()
115
+ if (existing?.hasValue && !isStale) return existing.value as T
116
+
117
+ // Refresh in place so get() keeps serving a stale value meanwhile.
118
+ const entry: Entry = existing ?? { hasValue: false }
119
+ const startedAt = now()
120
+ const promise = (async () => fn())()
121
+ entry.inflight = promise
122
+ touch(key, entry)
123
+
124
+ try {
125
+ const value = await promise
126
+ fill(entry, value, startedAt, setOptions)
127
+ return value
128
+ } catch (error) {
129
+ if (!hasFreshValue(entry) && store.get(key) === entry) {
130
+ store.delete(key)
131
+ }
132
+ throw error
133
+ } finally {
134
+ entry.inflight = undefined
135
+ }
136
+ },
137
+ get<T>(key: string): T | undefined {
138
+ const entry = read(key)
139
+ return hasFreshValue(entry) ? (entry?.value as T) : undefined
140
+ },
141
+ has(key: string): boolean {
142
+ return hasFreshValue(read(key))
143
+ },
144
+ set<T>(key: string, value: T, setOptions?: CacheSetOptions<T>): void {
145
+ touch(key, fill({ hasValue: false }, value, now(), setOptions))
146
+ },
147
+ delete(key: string): void {
148
+ store.delete(key)
149
+ },
150
+ clear(): void {
151
+ store.clear()
152
+ },
153
+ get size() {
154
+ return store.size
155
+ }
156
+ }
157
+ }