hanzio 1.1.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 +832 -219
- package/dist/api-wrapper/builders.d.ts +15 -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 +40 -2
- package/dist/api-wrapper/headers.d.ts +2 -1
- 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 +12 -10
- package/dist/api-wrapper/types.d.ts +104 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +45 -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/loaders.js → index-j1ybktbr.js} +608 -405
- 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/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +13 -25
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +8 -13
- package/dist/secrets/loaders.d.ts +5 -12
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1100
- 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 +863 -2809
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +261 -1377
- package/dist/src/secrets/vite.js +6 -17
- package/dist/src/secrets/worker.js +12 -0
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +347 -174
- 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 +37 -26
- 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 +87 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +84 -52
- package/src/api-wrapper/create-client.ts +246 -96
- 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 +117 -2
- package/src/api-wrapper/headers.ts +19 -1
- 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 +43 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +68 -74
- package/src/api-wrapper/types.ts +186 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +235 -83
- 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/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +79 -210
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +214 -66
- package/src/secrets/loaders.ts +24 -48
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- 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 +134 -103
- 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/infisical.js +0 -1194
- package/dist/zod/index.js +0 -210
package/src/cache/index.ts
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
value: T
|
|
3
|
-
timestamp: number
|
|
4
|
-
}
|
|
1
|
+
import { stableStringify } from '../string'
|
|
5
2
|
|
|
6
3
|
type AnyFunction = (...args: never[]) => unknown
|
|
7
|
-
type CachedFunction<
|
|
4
|
+
type CachedFunction<
|
|
5
|
+
T extends AnyFunction,
|
|
6
|
+
TPeek = Awaited<ReturnType<T>> | undefined
|
|
7
|
+
> = {
|
|
8
8
|
(...args: Parameters<T>): ReturnType<T>
|
|
9
9
|
clearCache: () => void
|
|
10
|
+
/** Drops the entry (and any in-flight call) for these arguments. */
|
|
11
|
+
invalidate: (...args: Parameters<T>) => void
|
|
12
|
+
/**
|
|
13
|
+
* What a call would return from the cache right now (including a stale
|
|
14
|
+
* value with refreshInBackground), without calling fn, starting a refresh
|
|
15
|
+
* or counting as a use for maxEntries. `undefined` on a miss. With a
|
|
16
|
+
* `store` this reads the store and returns a Promise.
|
|
17
|
+
*/
|
|
18
|
+
peek: (...args: Parameters<T>) => TPeek
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CacheStoreEntry = {
|
|
22
|
+
value: unknown
|
|
23
|
+
timestamp: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Contract for external cache stores (e.g. a Redis adapter).
|
|
28
|
+
* Serialization is the adapter's responsibility; this library never imports
|
|
29
|
+
* the underlying client.
|
|
30
|
+
*/
|
|
31
|
+
export type CacheStore = {
|
|
32
|
+
get: (key: string) => Promise<CacheStoreEntry | undefined>
|
|
33
|
+
/** `ttlMs` maps to the store's native expiry (e.g. Redis `PX`). */
|
|
34
|
+
set: (key: string, entry: CacheStoreEntry, ttlMs: number) => Promise<void>
|
|
35
|
+
delete: (key: string) => Promise<void>
|
|
36
|
+
/** Optional; called by clearCache when defined. */
|
|
37
|
+
clear?: () => Promise<void>
|
|
10
38
|
}
|
|
11
39
|
|
|
12
40
|
export type CacheFunctionOptions<T extends AnyFunction> = {
|
|
@@ -18,11 +46,29 @@ export type CacheFunctionOptions<T extends AnyFunction> = {
|
|
|
18
46
|
cacheKeyFn?: (...args: Parameters<T>) => unknown[]
|
|
19
47
|
/** Called when a background refresh fails (default: silent). */
|
|
20
48
|
onBackgroundRefreshError?: (error: unknown, context: { name: string }) => void
|
|
49
|
+
/**
|
|
50
|
+
* External cache store (e.g. a Redis adapter). Only supported for async
|
|
51
|
+
* functions; when omitted the built-in in-memory cache is used.
|
|
52
|
+
*/
|
|
53
|
+
store?: ReturnType<T> extends Promise<unknown> ? CacheStore : never
|
|
54
|
+
/** Called when a store get/set/delete/clear fails (default: silent). */
|
|
55
|
+
onStoreError?: (error: unknown, context: { name: string }) => void
|
|
56
|
+
/**
|
|
57
|
+
* Evicts the least recently used entry beyond this many entries (default:
|
|
58
|
+
* unbounded). Applies to the in-memory cache only.
|
|
59
|
+
*/
|
|
60
|
+
maxEntries?: number
|
|
21
61
|
}
|
|
22
62
|
|
|
63
|
+
export function cacheFunction<T extends AnyFunction>(
|
|
64
|
+
options: CacheFunctionOptions<T> & { store: CacheStore }
|
|
65
|
+
): CachedFunction<T, Promise<Awaited<ReturnType<T>> | undefined>>
|
|
23
66
|
export function cacheFunction<T extends AnyFunction>(
|
|
24
67
|
options: CacheFunctionOptions<T>
|
|
25
|
-
): CachedFunction<T>
|
|
68
|
+
): CachedFunction<T>
|
|
69
|
+
export function cacheFunction<T extends AnyFunction>(
|
|
70
|
+
options: CacheFunctionOptions<T>
|
|
71
|
+
): CachedFunction<T, unknown> {
|
|
26
72
|
const {
|
|
27
73
|
name,
|
|
28
74
|
fn,
|
|
@@ -30,115 +76,220 @@ export function cacheFunction<T extends AnyFunction>(
|
|
|
30
76
|
refreshInBackground = false,
|
|
31
77
|
cacheKeyArgs,
|
|
32
78
|
cacheKeyFn,
|
|
33
|
-
onBackgroundRefreshError
|
|
79
|
+
onBackgroundRefreshError,
|
|
80
|
+
store,
|
|
81
|
+
onStoreError,
|
|
82
|
+
maxEntries
|
|
34
83
|
} = options
|
|
35
84
|
|
|
36
|
-
const
|
|
85
|
+
const externalStore = store as CacheStore | undefined
|
|
86
|
+
|
|
87
|
+
const cache = new Map<string, CacheStoreEntry>()
|
|
37
88
|
const pendingPromises = new Map<string, Promise<unknown>>()
|
|
38
89
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
90
|
+
// Stale entries are kept for an extra TTL so refreshInBackground can serve
|
|
91
|
+
// them while revalidating.
|
|
92
|
+
const entryLifetimeMs = refreshInBackground ? cacheTimeMs * 2 : cacheTimeMs
|
|
93
|
+
|
|
94
|
+
let cleanupInterval: ReturnType<typeof setInterval> | undefined
|
|
95
|
+
|
|
96
|
+
const stopCleanup = () => {
|
|
97
|
+
if (cleanupInterval !== undefined) {
|
|
98
|
+
clearInterval(cleanupInterval)
|
|
99
|
+
cleanupInterval = undefined
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Created lazily on first write and unref'd so it never keeps the process
|
|
104
|
+
// alive. Stops itself once the cache is empty and restarts on the next
|
|
105
|
+
// write, so clearCache does not permanently disable sweeping.
|
|
106
|
+
const ensureCleanup = () => {
|
|
107
|
+
if (cleanupInterval !== undefined) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
cleanupInterval = setInterval(
|
|
111
|
+
() => {
|
|
112
|
+
const now = Date.now()
|
|
113
|
+
for (const [key, entry] of cache.entries()) {
|
|
114
|
+
if (now - entry.timestamp >= entryLifetimeMs) {
|
|
115
|
+
cache.delete(key)
|
|
116
|
+
}
|
|
45
117
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
118
|
+
if (cache.size === 0) {
|
|
119
|
+
stopCleanup()
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
Math.min(cacheTimeMs / 2, 5 * 60 * 1000)
|
|
123
|
+
)
|
|
124
|
+
cleanupInterval.unref?.()
|
|
125
|
+
}
|
|
50
126
|
|
|
51
|
-
const
|
|
127
|
+
const buildKey = (args: Parameters<T>): string => {
|
|
52
128
|
const keyArgs = cacheKeyFn
|
|
53
129
|
? cacheKeyFn(...args)
|
|
54
130
|
: cacheKeyArgs
|
|
55
131
|
? cacheKeyArgs.map((i) => args[i])
|
|
56
132
|
: args
|
|
57
|
-
|
|
58
|
-
|
|
133
|
+
return `${name}:${stableStringify(keyArgs)}`
|
|
134
|
+
}
|
|
59
135
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
136
|
+
const isFresh = (entry: CacheStoreEntry) =>
|
|
137
|
+
Date.now() - entry.timestamp < cacheTimeMs
|
|
138
|
+
|
|
139
|
+
// The value a call would return straight from the cache, if any.
|
|
140
|
+
const servable = (entry: CacheStoreEntry | undefined) =>
|
|
141
|
+
entry && (refreshInBackground || isFresh(entry)) ? entry : undefined
|
|
142
|
+
|
|
143
|
+
// Map keeps insertion order, so re-inserting on every hit and write makes
|
|
144
|
+
// the first key the least recently used.
|
|
145
|
+
const touch = (key: string, entry: CacheStoreEntry) => {
|
|
146
|
+
cache.delete(key)
|
|
147
|
+
cache.set(key, entry)
|
|
148
|
+
if (maxEntries !== undefined && cache.size > maxEntries) {
|
|
149
|
+
cache.delete(cache.keys().next().value as string)
|
|
63
150
|
}
|
|
151
|
+
}
|
|
64
152
|
|
|
65
|
-
|
|
153
|
+
const guardStore = async <R>(
|
|
154
|
+
operation: () => Promise<R>
|
|
155
|
+
): Promise<R | undefined> => {
|
|
156
|
+
try {
|
|
157
|
+
return await operation()
|
|
158
|
+
} catch (error) {
|
|
159
|
+
onStoreError?.(error, { name })
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
}
|
|
66
163
|
|
|
67
|
-
|
|
68
|
-
|
|
164
|
+
const save = (key: string, value: unknown) => {
|
|
165
|
+
const entry = { value, timestamp: Date.now() }
|
|
166
|
+
if (externalStore) {
|
|
167
|
+
return guardStore(() => externalStore.set(key, entry, entryLifetimeMs))
|
|
168
|
+
}
|
|
169
|
+
touch(key, entry)
|
|
170
|
+
ensureCleanup()
|
|
171
|
+
}
|
|
69
172
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
173
|
+
// Calls fn and saves the result. Async calls are tracked in
|
|
174
|
+
// pendingPromises for dedupe. A failed background refresh (`stale` given)
|
|
175
|
+
// resolves to the stale value instead of rejecting.
|
|
176
|
+
const run = (
|
|
177
|
+
key: string,
|
|
178
|
+
args: Parameters<T>,
|
|
179
|
+
stale?: CacheStoreEntry
|
|
180
|
+
): unknown => {
|
|
181
|
+
const result = fn(...args)
|
|
76
182
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
183
|
+
if (!isPromiseLike(result)) {
|
|
184
|
+
save(key, result)
|
|
185
|
+
return result
|
|
186
|
+
}
|
|
82
187
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
.then((resolvedValue) => {
|
|
90
|
-
cache.set(key, { value: resolvedValue, timestamp: now })
|
|
91
|
-
pendingPromises.delete(key)
|
|
92
|
-
return resolvedValue
|
|
93
|
-
})
|
|
94
|
-
.catch((error: unknown) => {
|
|
95
|
-
pendingPromises.delete(key)
|
|
96
|
-
onBackgroundRefreshError?.(error, { name })
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
pendingPromises.set(key, backgroundPromise)
|
|
100
|
-
} else {
|
|
101
|
-
cache.set(key, {
|
|
102
|
-
value: backgroundUpdate as ReturnType<T>,
|
|
103
|
-
timestamp: now
|
|
104
|
-
})
|
|
188
|
+
const promise: Promise<unknown> = Promise.resolve(result)
|
|
189
|
+
.then(
|
|
190
|
+
async (value) => {
|
|
191
|
+
// Skip the save when invalidate/clearCache ran meanwhile.
|
|
192
|
+
if (pendingPromises.get(key) === promise) {
|
|
193
|
+
await save(key, value)
|
|
105
194
|
}
|
|
106
|
-
|
|
107
|
-
|
|
195
|
+
return value
|
|
196
|
+
},
|
|
197
|
+
(error: unknown) => {
|
|
198
|
+
if (!stale) throw error
|
|
199
|
+
onBackgroundRefreshError?.(error, { name })
|
|
200
|
+
return stale.value
|
|
108
201
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
202
|
+
)
|
|
203
|
+
.finally(() => {
|
|
204
|
+
if (pendingPromises.get(key) === promise) {
|
|
205
|
+
pendingPromises.delete(key)
|
|
112
206
|
}
|
|
113
|
-
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
pendingPromises.set(key, promise)
|
|
210
|
+
return promise
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Serves a cached entry, starting a background refresh when it is stale.
|
|
214
|
+
const serve = (key: string, args: Parameters<T>, entry: CacheStoreEntry) => {
|
|
215
|
+
if (!isFresh(entry) && !pendingPromises.has(key)) {
|
|
216
|
+
run(key, args, entry)
|
|
114
217
|
}
|
|
218
|
+
return entry.value
|
|
219
|
+
}
|
|
115
220
|
|
|
116
|
-
|
|
221
|
+
const callWithStore = (
|
|
222
|
+
cacheStore: CacheStore,
|
|
223
|
+
key: string,
|
|
224
|
+
args: Parameters<T>
|
|
225
|
+
): Promise<unknown> => {
|
|
226
|
+
const pending = pendingPromises.get(key)
|
|
227
|
+
// Background refresh callers can still use an entry from the store.
|
|
228
|
+
if (pending && !refreshInBackground) {
|
|
229
|
+
return pending
|
|
230
|
+
}
|
|
117
231
|
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
cache.set(key, { value: resolvedValue, timestamp: now })
|
|
122
|
-
pendingPromises.delete(key)
|
|
123
|
-
return resolvedValue
|
|
124
|
-
})
|
|
125
|
-
.catch((error) => {
|
|
126
|
-
pendingPromises.delete(key)
|
|
127
|
-
throw error
|
|
128
|
-
})
|
|
232
|
+
return (async () => {
|
|
233
|
+
const entry = await guardStore(() => cacheStore.get(key))
|
|
234
|
+
const cached = servable(entry)
|
|
129
235
|
|
|
130
|
-
|
|
131
|
-
|
|
236
|
+
if (cached) {
|
|
237
|
+
return serve(key, args, cached)
|
|
238
|
+
}
|
|
239
|
+
if (entry) {
|
|
240
|
+
await guardStore(() => cacheStore.delete(key))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// A concurrent caller may have started a fetch while we awaited the
|
|
244
|
+
// store lookup.
|
|
245
|
+
return pendingPromises.get(key) ?? run(key, args)
|
|
246
|
+
})()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const wrappedFn = (...args: Parameters<T>): ReturnType<T> => {
|
|
250
|
+
const key = buildKey(args)
|
|
251
|
+
|
|
252
|
+
if (externalStore) {
|
|
253
|
+
return callWithStore(externalStore, key, args) as ReturnType<T>
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const cached = servable(cache.get(key))
|
|
257
|
+
if (cached) {
|
|
258
|
+
touch(key, cached)
|
|
259
|
+
return serve(key, args, cached) as ReturnType<T>
|
|
132
260
|
}
|
|
261
|
+
cache.delete(key)
|
|
133
262
|
|
|
134
|
-
|
|
135
|
-
return result as ReturnType<T>
|
|
263
|
+
return (pendingPromises.get(key) ?? run(key, args)) as ReturnType<T>
|
|
136
264
|
}
|
|
137
265
|
|
|
138
266
|
wrappedFn.clearCache = () => {
|
|
139
267
|
cache.clear()
|
|
140
268
|
pendingPromises.clear()
|
|
141
|
-
|
|
269
|
+
stopCleanup()
|
|
270
|
+
const clearStore = externalStore?.clear?.bind(externalStore)
|
|
271
|
+
if (clearStore) {
|
|
272
|
+
void guardStore(clearStore)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
wrappedFn.invalidate = (...args: Parameters<T>) => {
|
|
277
|
+
const key = buildKey(args)
|
|
278
|
+
cache.delete(key)
|
|
279
|
+
pendingPromises.delete(key)
|
|
280
|
+
if (externalStore) {
|
|
281
|
+
void guardStore(() => externalStore.delete(key))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
wrappedFn.peek = (...args: Parameters<T>): unknown => {
|
|
286
|
+
const key = buildKey(args)
|
|
287
|
+
if (externalStore) {
|
|
288
|
+
return guardStore(() => externalStore.get(key)).then(
|
|
289
|
+
(entry) => servable(entry)?.value
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
return servable(cache.get(key))?.value
|
|
142
293
|
}
|
|
143
294
|
|
|
144
295
|
return wrappedFn
|
|
@@ -152,3 +303,4 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
|
152
303
|
typeof value.then === 'function'
|
|
153
304
|
)
|
|
154
305
|
}
|
|
306
|
+
export * from './createCache'
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { clamp } from '../math/scale'
|
|
2
|
+
|
|
3
|
+
/** An sRGB color: `r`/`g`/`b` in 0..255, `a` in 0..1. */
|
|
4
|
+
export type Rgba = { r: number; g: number; b: number; a: number }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Splits `rgb(…)`/`rgba(…)`/`hsl(…)`/`hsla(…)` (comma, space or slash syntax)
|
|
8
|
+
* into its name and 3-4 raw arguments. Internal; shared with `withAlpha`.
|
|
9
|
+
*/
|
|
10
|
+
export function colorFunctionArgs(
|
|
11
|
+
color: string
|
|
12
|
+
): { name: 'rgb' | 'hsl'; args: string[] } | undefined {
|
|
13
|
+
const match = /^(rgb|hsl)a?\(([^()]*)\)$/i.exec(color.trim())
|
|
14
|
+
const args = match?.[2]?.trim().split(/[\s,/]+/)
|
|
15
|
+
if (!match || !args || (args.length !== 3 && args.length !== 4)) {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
return { name: match[1]!.toLowerCase() as 'rgb' | 'hsl', args }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A number, or a percentage scaled so 100% = `percentOf`.
|
|
22
|
+
function parseChannel(raw: string, percentOf: number): number | undefined {
|
|
23
|
+
const isPercent = raw.endsWith('%')
|
|
24
|
+
const text = isPercent ? raw.slice(0, -1) : raw
|
|
25
|
+
const n = text === '' ? Number.NaN : Number(text)
|
|
26
|
+
if (!Number.isFinite(n)) return undefined
|
|
27
|
+
return isPercent ? (n / 100) * percentOf : n
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|
31
|
+
const a = s * Math.min(l, 1 - l)
|
|
32
|
+
const f = (n: number) => {
|
|
33
|
+
const k = (n + h / 30) % 12
|
|
34
|
+
return (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1))) * 255
|
|
35
|
+
}
|
|
36
|
+
return [f(0), f(8), f(4)]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`/`rgba()` and
|
|
41
|
+
* `hsl()`/`hsla()` (comma or space/slash syntax, percentages allowed).
|
|
42
|
+
* Returns `undefined` for anything else (named colors, CSS variables, …).
|
|
43
|
+
*/
|
|
44
|
+
export function parseColor(color: string): Rgba | undefined {
|
|
45
|
+
const trimmed = color.trim()
|
|
46
|
+
|
|
47
|
+
const hex = /^#([\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i.exec(trimmed)?.[1]
|
|
48
|
+
if (hex) {
|
|
49
|
+
const full = hex.length <= 4 ? hex.replace(/./g, '$&$&') : hex
|
|
50
|
+
const [r = 0, g = 0, b = 0, a = 255] = [0, 2, 4, 6].map((i) =>
|
|
51
|
+
i < full.length ? Number.parseInt(full.slice(i, i + 2), 16) : undefined
|
|
52
|
+
)
|
|
53
|
+
return { r, g, b, a: a / 255 }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const fn = colorFunctionArgs(trimmed)
|
|
57
|
+
if (!fn) return undefined
|
|
58
|
+
const [x = '', y = '', z = '', alpha = '1'] = fn.args
|
|
59
|
+
const isRgb = fn.name === 'rgb'
|
|
60
|
+
const c1 = parseChannel(isRgb ? x : x.replace(/deg$/i, ''), isRgb ? 255 : 360)
|
|
61
|
+
const c2 = parseChannel(y, isRgb ? 255 : 100)
|
|
62
|
+
const c3 = parseChannel(z, isRgb ? 255 : 100)
|
|
63
|
+
const a = parseChannel(alpha, 1)
|
|
64
|
+
if (c1 === undefined || c2 === undefined || c3 === undefined) return undefined
|
|
65
|
+
if (a === undefined) return undefined
|
|
66
|
+
|
|
67
|
+
const [r, g, b] = isRgb
|
|
68
|
+
? [c1, c2, c3]
|
|
69
|
+
: hslToRgb(
|
|
70
|
+
((c1 % 360) + 360) % 360,
|
|
71
|
+
clamp(c2 / 100, 0, 1),
|
|
72
|
+
clamp(c3 / 100, 0, 1)
|
|
73
|
+
)
|
|
74
|
+
const channel = (v: number) => Math.round(clamp(v, 0, 255))
|
|
75
|
+
return { r: channel(r), g: channel(g), b: channel(b), a: clamp(a, 0, 1) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toRgba(color: Rgba | string): Rgba {
|
|
79
|
+
if (typeof color !== 'string') return color
|
|
80
|
+
const parsed = parseColor(color)
|
|
81
|
+
if (!parsed) throw new TypeError(`Unsupported color: ${color}`)
|
|
82
|
+
return parsed
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `#rrggbb` (or `#rrggbbaa` with `alpha: true`). Throws on unparseable strings. */
|
|
86
|
+
export function toHex(
|
|
87
|
+
color: Rgba | string,
|
|
88
|
+
options: { alpha?: boolean } = {}
|
|
89
|
+
): string {
|
|
90
|
+
const { r, g, b, a } = toRgba(color)
|
|
91
|
+
const channels = options.alpha ? [r, g, b, a * 255] : [r, g, b]
|
|
92
|
+
return `#${channels
|
|
93
|
+
.map((v) =>
|
|
94
|
+
Math.round(clamp(v, 0, 255))
|
|
95
|
+
.toString(16)
|
|
96
|
+
.padStart(2, '0')
|
|
97
|
+
)
|
|
98
|
+
.join('')}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `rgb(r, g, b)`, or `rgba(r, g, b, a)` when translucent. Throws on unparseable strings. */
|
|
102
|
+
export function toRgbString(color: Rgba | string): string {
|
|
103
|
+
const { r, g, b, a } = toRgba(color)
|
|
104
|
+
const [rr, gg, bb] = [r, g, b].map((v) => Math.round(clamp(v, 0, 255)))
|
|
105
|
+
return a < 1 ? `rgba(${rr}, ${gg}, ${bb}, ${a})` : `rgb(${rr}, ${gg}, ${bb})`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** WCAG 2.x relative luminance (0 = black, 1 = white); alpha is ignored. */
|
|
109
|
+
export function relativeLuminance(color: Rgba | string): number {
|
|
110
|
+
const { r, g, b } = toRgba(color)
|
|
111
|
+
const [lr, lg, lb] = [r, g, b].map((v) => {
|
|
112
|
+
const c = clamp(v, 0, 255) / 255
|
|
113
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
|
|
114
|
+
}) as [number, number, number]
|
|
115
|
+
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** WCAG contrast ratio between two colors, from 1 to 21. */
|
|
119
|
+
export function contrastRatio(a: Rgba | string, b: Rgba | string): number {
|
|
120
|
+
const la = relativeLuminance(a)
|
|
121
|
+
const lb = relativeLuminance(b)
|
|
122
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Whichever of `light`/`dark` contrasts more with `background` (ties go to `light`). */
|
|
126
|
+
export function contrastText(
|
|
127
|
+
background: Rgba | string,
|
|
128
|
+
options: { light?: string; dark?: string } = {}
|
|
129
|
+
): string {
|
|
130
|
+
const { light = '#ffffff', dark = '#000000' } = options
|
|
131
|
+
return contrastRatio(background, light) >= contrastRatio(background, dark)
|
|
132
|
+
? light
|
|
133
|
+
: dark
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mix(a: Rgba, b: Rgba, weight: number): Rgba {
|
|
137
|
+
const t = clamp(weight, 0, 1)
|
|
138
|
+
const at = (x: number, y: number) => x + (y - x) * t
|
|
139
|
+
return {
|
|
140
|
+
r: at(a.r, b.r),
|
|
141
|
+
g: at(a.g, b.g),
|
|
142
|
+
b: at(a.b, b.b),
|
|
143
|
+
a: at(a.a, b.a)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const hexOf = (color: Rgba) => toHex(color, { alpha: color.a < 1 })
|
|
148
|
+
|
|
149
|
+
/** Blend `b` into `a` by `weight` (0 = `a`, 1 = `b`) as hex; alpha kept only when translucent. */
|
|
150
|
+
export function mixColors(
|
|
151
|
+
a: Rgba | string,
|
|
152
|
+
b: Rgba | string,
|
|
153
|
+
weight: number
|
|
154
|
+
): string {
|
|
155
|
+
return hexOf(mix(toRgba(a), toRgba(b), weight))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Darken toward black (`amount` < 0) or lighten toward white (> 0); `amount` in -1..1. Returns hex. */
|
|
159
|
+
export function shade(color: Rgba | string, amount: number): string {
|
|
160
|
+
const base = toRgba(color)
|
|
161
|
+
const v = amount < 0 ? 0 : 255
|
|
162
|
+
return hexOf(mix(base, { r: v, g: v, b: v, a: base.a }, Math.abs(amount)))
|
|
163
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
contrastRatio,
|
|
3
|
+
contrastText,
|
|
4
|
+
mixColors,
|
|
5
|
+
parseColor,
|
|
6
|
+
type Rgba,
|
|
7
|
+
relativeLuminance,
|
|
8
|
+
shade,
|
|
9
|
+
toHex,
|
|
10
|
+
toRgbString
|
|
11
|
+
} from './contrast'
|
|
12
|
+
export {
|
|
13
|
+
createResolveColorsByKeys,
|
|
14
|
+
getHeatmapCellStyle,
|
|
15
|
+
hashStringToColorIndex,
|
|
16
|
+
METRICS_SERIES_PALETTE_SIZE,
|
|
17
|
+
pickFromPalette,
|
|
18
|
+
type ResolveColorsByKeysResult,
|
|
19
|
+
resolveSeriesColorMap,
|
|
20
|
+
seriesColorKey,
|
|
21
|
+
withAlpha
|
|
22
|
+
} from './metricsSeriesColors'
|