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.
- package/README.md +786 -239
- package/dist/api-wrapper/builders.d.ts +4 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +39 -2
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +4 -5
- package/dist/api-wrapper/types.d.ts +103 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +19 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1130
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +739 -3145
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +44 -1428
- package/dist/src/secrets/vite.js +6 -39
- package/dist/src/secrets/worker.js +10 -273
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +340 -167
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +31 -24
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +78 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +32 -25
- package/src/api-wrapper/create-client.ts +171 -41
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +107 -2
- package/src/api-wrapper/headers.ts +11 -6
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +42 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +41 -43
- package/src/api-wrapper/types.ts +185 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +132 -147
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +58 -38
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/loaders.js +0 -1529
- package/dist/zod/index.js +0 -210
package/src/array/index.ts
CHANGED
|
@@ -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
|
-
|
|
29
|
-
TCurrent
|
|
30
|
-
TNext
|
|
31
|
-
|
|
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:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
96
|
+
const difference = {
|
|
62
97
|
new: newItems,
|
|
63
98
|
upsert: upsertItems,
|
|
64
|
-
delete:
|
|
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 =
|
|
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
|
|
124
|
+
export function getUniqueValuesByKey<T>(
|
|
86
125
|
array: readonly T[],
|
|
87
|
-
|
|
126
|
+
keyOrFn: keyof T | ((item: T) => unknown)
|
|
88
127
|
): T[] {
|
|
89
|
-
const seen = new Set<
|
|
128
|
+
const seen = new Set<unknown>()
|
|
90
129
|
const result: T[] = []
|
|
91
130
|
|
|
92
131
|
for (const item of array) {
|
|
93
|
-
const value = item[
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|