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/cache/index.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
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
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
export type CacheStoreEntry = {
|
|
@@ -44,11 +53,22 @@ export type CacheFunctionOptions<T extends AnyFunction> = {
|
|
|
44
53
|
store?: ReturnType<T> extends Promise<unknown> ? CacheStore : never
|
|
45
54
|
/** Called when a store get/set/delete/clear fails (default: silent). */
|
|
46
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
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
export function cacheFunction<T extends AnyFunction>(
|
|
64
|
+
options: CacheFunctionOptions<T> & { store: CacheStore }
|
|
65
|
+
): CachedFunction<T, Promise<Awaited<ReturnType<T>> | undefined>>
|
|
66
|
+
export function cacheFunction<T extends AnyFunction>(
|
|
67
|
+
options: CacheFunctionOptions<T>
|
|
68
|
+
): CachedFunction<T>
|
|
49
69
|
export function cacheFunction<T extends AnyFunction>(
|
|
50
70
|
options: CacheFunctionOptions<T>
|
|
51
|
-
): CachedFunction<T> {
|
|
71
|
+
): CachedFunction<T, unknown> {
|
|
52
72
|
const {
|
|
53
73
|
name,
|
|
54
74
|
fn,
|
|
@@ -58,12 +78,13 @@ export function cacheFunction<T extends AnyFunction>(
|
|
|
58
78
|
cacheKeyFn,
|
|
59
79
|
onBackgroundRefreshError,
|
|
60
80
|
store,
|
|
61
|
-
onStoreError
|
|
81
|
+
onStoreError,
|
|
82
|
+
maxEntries
|
|
62
83
|
} = options
|
|
63
84
|
|
|
64
85
|
const externalStore = store as CacheStore | undefined
|
|
65
86
|
|
|
66
|
-
const cache = new Map<string,
|
|
87
|
+
const cache = new Map<string, CacheStoreEntry>()
|
|
67
88
|
const pendingPromises = new Map<string, Promise<unknown>>()
|
|
68
89
|
|
|
69
90
|
// Stale entries are kept for an extra TTL so refreshInBackground can serve
|
|
@@ -109,91 +130,99 @@ export function cacheFunction<T extends AnyFunction>(
|
|
|
109
130
|
: cacheKeyArgs
|
|
110
131
|
? cacheKeyArgs.map((i) => args[i])
|
|
111
132
|
: args
|
|
112
|
-
return `${name}:${
|
|
133
|
+
return `${name}:${stableStringify(keyArgs)}`
|
|
113
134
|
}
|
|
114
135
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
ensureCleanup()
|
|
118
|
-
}
|
|
136
|
+
const isFresh = (entry: CacheStoreEntry) =>
|
|
137
|
+
Date.now() - entry.timestamp < cacheTimeMs
|
|
119
138
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
): ReturnType<T> => {
|
|
124
|
-
const result = fn(...args)
|
|
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
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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)
|
|
129
150
|
}
|
|
151
|
+
}
|
|
130
152
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
163
|
|
|
142
|
-
|
|
143
|
-
|
|
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()
|
|
144
171
|
}
|
|
145
172
|
|
|
146
|
-
|
|
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 = (
|
|
147
177
|
key: string,
|
|
148
178
|
args: Parameters<T>,
|
|
149
|
-
|
|
150
|
-
) => {
|
|
179
|
+
stale?: CacheStoreEntry
|
|
180
|
+
): unknown => {
|
|
151
181
|
const result = fn(...args)
|
|
152
182
|
|
|
153
183
|
if (!isPromiseLike(result)) {
|
|
154
|
-
|
|
155
|
-
return
|
|
184
|
+
save(key, result)
|
|
185
|
+
return result
|
|
156
186
|
}
|
|
157
187
|
|
|
158
|
-
const promise = (result
|
|
159
|
-
.then(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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)
|
|
194
|
+
}
|
|
195
|
+
return value
|
|
196
|
+
},
|
|
197
|
+
(error: unknown) => {
|
|
198
|
+
if (!stale) throw error
|
|
199
|
+
onBackgroundRefreshError?.(error, { name })
|
|
200
|
+
return stale.value
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
.finally(() => {
|
|
204
|
+
if (pendingPromises.get(key) === promise) {
|
|
205
|
+
pendingPromises.delete(key)
|
|
206
|
+
}
|
|
170
207
|
})
|
|
171
208
|
|
|
172
209
|
pendingPromises.set(key, promise)
|
|
210
|
+
return promise
|
|
173
211
|
}
|
|
174
212
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
key
|
|
178
|
-
|
|
179
|
-
) => {
|
|
180
|
-
try {
|
|
181
|
-
await cacheStore.set(
|
|
182
|
-
key,
|
|
183
|
-
{ value, timestamp: Date.now() },
|
|
184
|
-
entryLifetimeMs
|
|
185
|
-
)
|
|
186
|
-
} catch (error) {
|
|
187
|
-
onStoreError?.(error, { name })
|
|
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)
|
|
188
217
|
}
|
|
218
|
+
return entry.value
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
const callWithStore = (
|
|
192
222
|
cacheStore: CacheStore,
|
|
223
|
+
key: string,
|
|
193
224
|
args: Parameters<T>
|
|
194
225
|
): Promise<unknown> => {
|
|
195
|
-
const key = buildKey(args)
|
|
196
|
-
|
|
197
226
|
const pending = pendingPromises.get(key)
|
|
198
227
|
// Background refresh callers can still use an entry from the store.
|
|
199
228
|
if (pending && !refreshInBackground) {
|
|
@@ -201,111 +230,66 @@ export function cacheFunction<T extends AnyFunction>(
|
|
|
201
230
|
}
|
|
202
231
|
|
|
203
232
|
return (async () => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
entry = await cacheStore.get(key)
|
|
207
|
-
} catch (error) {
|
|
208
|
-
onStoreError?.(error, { name })
|
|
209
|
-
}
|
|
233
|
+
const entry = await guardStore(() => cacheStore.get(key))
|
|
234
|
+
const cached = servable(entry)
|
|
210
235
|
|
|
236
|
+
if (cached) {
|
|
237
|
+
return serve(key, args, cached)
|
|
238
|
+
}
|
|
211
239
|
if (entry) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (isFresh) {
|
|
215
|
-
return entry.value
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (refreshInBackground) {
|
|
219
|
-
if (!pendingPromises.has(key)) {
|
|
220
|
-
const staleValue = entry.value
|
|
221
|
-
const refresh = (fn(...args) as Promise<unknown>)
|
|
222
|
-
.then(async (value) => {
|
|
223
|
-
await saveToStore(cacheStore, key, value)
|
|
224
|
-
pendingPromises.delete(key)
|
|
225
|
-
return value
|
|
226
|
-
})
|
|
227
|
-
.catch((error: unknown) => {
|
|
228
|
-
pendingPromises.delete(key)
|
|
229
|
-
onBackgroundRefreshError?.(error, { name })
|
|
230
|
-
return staleValue
|
|
231
|
-
})
|
|
232
|
-
pendingPromises.set(key, refresh)
|
|
233
|
-
}
|
|
234
|
-
return entry.value
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
await cacheStore.delete(key)
|
|
239
|
-
} catch (error) {
|
|
240
|
-
onStoreError?.(error, { name })
|
|
241
|
-
}
|
|
240
|
+
await guardStore(() => cacheStore.delete(key))
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
// A concurrent caller may have started a fetch while we awaited the
|
|
245
244
|
// store lookup.
|
|
246
|
-
|
|
247
|
-
if (existing) {
|
|
248
|
-
return existing
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const promise = (fn(...args) as Promise<unknown>)
|
|
252
|
-
.then(async (value) => {
|
|
253
|
-
await saveToStore(cacheStore, key, value)
|
|
254
|
-
pendingPromises.delete(key)
|
|
255
|
-
return value
|
|
256
|
-
})
|
|
257
|
-
.catch((error: unknown) => {
|
|
258
|
-
pendingPromises.delete(key)
|
|
259
|
-
throw error
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
pendingPromises.set(key, promise)
|
|
263
|
-
return promise
|
|
245
|
+
return pendingPromises.get(key) ?? run(key, args)
|
|
264
246
|
})()
|
|
265
247
|
}
|
|
266
248
|
|
|
267
249
|
const wrappedFn = (...args: Parameters<T>): ReturnType<T> => {
|
|
268
|
-
if (externalStore) {
|
|
269
|
-
return callWithStore(externalStore, args) as ReturnType<T>
|
|
270
|
-
}
|
|
271
|
-
|
|
272
250
|
const key = buildKey(args)
|
|
273
|
-
const cached = cache.get(key)
|
|
274
|
-
|
|
275
|
-
if (cached) {
|
|
276
|
-
const isFresh = Date.now() - cached.timestamp < cacheTimeMs
|
|
277
|
-
|
|
278
|
-
if (isFresh) {
|
|
279
|
-
return cached.value
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (refreshInBackground) {
|
|
283
|
-
if (!pendingPromises.has(key)) {
|
|
284
|
-
startBackgroundRefresh(key, args, cached.value)
|
|
285
|
-
}
|
|
286
|
-
return cached.value
|
|
287
|
-
}
|
|
288
251
|
|
|
289
|
-
|
|
252
|
+
if (externalStore) {
|
|
253
|
+
return callWithStore(externalStore, key, args) as ReturnType<T>
|
|
290
254
|
}
|
|
291
255
|
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
|
|
256
|
+
const cached = servable(cache.get(key))
|
|
257
|
+
if (cached) {
|
|
258
|
+
touch(key, cached)
|
|
259
|
+
return serve(key, args, cached) as ReturnType<T>
|
|
295
260
|
}
|
|
261
|
+
cache.delete(key)
|
|
296
262
|
|
|
297
|
-
return
|
|
263
|
+
return (pendingPromises.get(key) ?? run(key, args)) as ReturnType<T>
|
|
298
264
|
}
|
|
299
265
|
|
|
300
266
|
wrappedFn.clearCache = () => {
|
|
301
267
|
cache.clear()
|
|
302
268
|
pendingPromises.clear()
|
|
303
269
|
stopCleanup()
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
)
|
|
308
291
|
}
|
|
292
|
+
return servable(cache.get(key))?.value
|
|
309
293
|
}
|
|
310
294
|
|
|
311
295
|
return wrappedFn
|
|
@@ -319,3 +303,4 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
|
319
303
|
typeof value.then === 'function'
|
|
320
304
|
)
|
|
321
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'
|