hanzio 1.2.0 → 2.1.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 (155) 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-8r2vdzjg.js +87 -0
  36. package/dist/index-hy7kbb99.js +157 -0
  37. package/dist/index-hyt6c56h.js +290 -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-ktynw3vs.js +37 -0
  42. package/dist/index-mnkcfyx8.js +78 -0
  43. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  44. package/dist/index-rp5pqghz.js +157 -0
  45. package/dist/index-w882zry3.js +29 -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/secrets/core.d.ts +7 -0
  55. package/dist/secrets/index.d.ts +3 -1
  56. package/dist/secrets/public-config.d.ts +7 -0
  57. package/dist/secrets/vite.d.ts +1 -0
  58. package/dist/secrets/worker.d.ts +1 -0
  59. package/dist/sitemap/index.d.ts +48 -6
  60. package/dist/src/api-wrapper/index.js +60 -1130
  61. package/dist/src/api-wrapper/testing.js +20 -0
  62. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  63. package/dist/src/crypto/index.js +195 -0
  64. package/dist/src/format/index.js +211 -0
  65. package/dist/src/i18n/index.js +36 -0
  66. package/dist/src/index.js +739 -3145
  67. package/dist/src/jwt/index.js +138 -92
  68. package/dist/src/p-queue/index.js +9 -437
  69. package/dist/src/secrets/index.js +50 -1428
  70. package/dist/src/secrets/vite.js +10 -39
  71. package/dist/src/secrets/worker.js +14 -273
  72. package/dist/src/sitemap/index.js +183 -511
  73. package/dist/src/storage/index.js +119 -0
  74. package/dist/src/zod/index.js +340 -167
  75. package/dist/state/index.d.ts +21 -0
  76. package/dist/storage/index.d.ts +58 -0
  77. package/dist/string/index.d.ts +75 -2
  78. package/dist/typedswitch/index.d.ts +31 -24
  79. package/dist/types/index.d.ts +11 -0
  80. package/dist/url/index.d.ts +50 -1
  81. package/dist/zod/index.d.ts +32 -3
  82. package/dist/zod/introspect.d.ts +19 -0
  83. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  84. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  85. package/dist/zod/zodToTypeString.d.ts +8 -3
  86. package/package.json +78 -7
  87. package/src/api-wrapper/actions-impl.ts +11 -72
  88. package/src/api-wrapper/builders.ts +32 -25
  89. package/src/api-wrapper/create-client.ts +171 -41
  90. package/src/api-wrapper/describe.ts +157 -0
  91. package/src/api-wrapper/endpoint.ts +26 -0
  92. package/src/api-wrapper/error-reporting.ts +7 -1
  93. package/src/api-wrapper/errors.ts +107 -2
  94. package/src/api-wrapper/headers.ts +11 -6
  95. package/src/api-wrapper/index.ts +5 -0
  96. package/src/api-wrapper/otel.ts +139 -0
  97. package/src/api-wrapper/paginate.ts +28 -0
  98. package/src/api-wrapper/redaction.ts +42 -0
  99. package/src/api-wrapper/shared.ts +21 -2
  100. package/src/api-wrapper/testing.ts +48 -0
  101. package/src/api-wrapper/transport.ts +41 -43
  102. package/src/api-wrapper/types.ts +185 -18
  103. package/src/array/index.ts +195 -42
  104. package/src/cache/createCache.ts +157 -0
  105. package/src/cache/index.ts +132 -147
  106. package/src/color/contrast.ts +163 -0
  107. package/src/color/index.ts +22 -0
  108. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  109. package/src/cool-console-log/index.ts +128 -41
  110. package/src/crypto/encoding.ts +83 -0
  111. package/src/crypto/hash.ts +170 -0
  112. package/src/crypto/index.ts +26 -0
  113. package/src/crypto/sealer.ts +168 -0
  114. package/src/crypto/webhook.ts +86 -0
  115. package/src/date/index.ts +109 -34
  116. package/src/emitter/index.ts +82 -0
  117. package/src/error/index.ts +63 -0
  118. package/src/format/index.ts +389 -0
  119. package/src/function/index.ts +201 -0
  120. package/src/guard/index.ts +80 -0
  121. package/src/i18n/index.ts +125 -0
  122. package/src/index.ts +11 -7
  123. package/src/jwt/index.ts +199 -125
  124. package/src/math/index.ts +5 -10
  125. package/src/math/linearRegressionTrend.ts +33 -8
  126. package/src/math/scale.ts +17 -0
  127. package/src/object/index.ts +99 -0
  128. package/src/p-queue/index.ts +2 -0
  129. package/src/promise/index.ts +304 -9
  130. package/src/secrets/core.ts +72 -16
  131. package/src/secrets/index.ts +16 -4
  132. package/src/secrets/public-config.ts +47 -0
  133. package/src/secrets/vite.ts +2 -0
  134. package/src/secrets/worker.ts +2 -0
  135. package/src/sitemap/index.ts +293 -130
  136. package/src/state/index.ts +133 -81
  137. package/src/storage/index.ts +187 -0
  138. package/src/string/index.ts +261 -23
  139. package/src/typedswitch/index.ts +58 -38
  140. package/src/types/index.ts +14 -0
  141. package/src/url/index.ts +146 -1
  142. package/src/zod/index.ts +105 -9
  143. package/src/zod/introspect.ts +103 -0
  144. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  145. package/src/zod/zodMinMaxFilter.ts +37 -13
  146. package/src/zod/zodToTypeString.ts +184 -181
  147. package/dist/api-wrapper/index.js +0 -250
  148. package/dist/gzip/index.d.ts +0 -2
  149. package/dist/gzip/index.js +0 -16
  150. package/dist/index.js +0 -1823
  151. package/dist/jwt/index.js +0 -19
  152. package/dist/sitemap/index.js +0 -552
  153. package/dist/src/gzip/index.js +0 -16
  154. package/dist/src/secrets/loaders.js +0 -1529
  155. package/dist/zod/index.js +0 -210
@@ -1,12 +1,21 @@
1
- type CacheEntry<T> = {
2
- value: T
3
- timestamp: number
4
- }
1
+ import { stableStringify } from '../string'
5
2
 
6
3
  type AnyFunction = (...args: never[]) => unknown
7
- type CachedFunction<T extends AnyFunction> = {
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, CacheEntry<ReturnType<T>>>()
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}:${JSON.stringify(keyArgs)}`
133
+ return `${name}:${stableStringify(keyArgs)}`
113
134
  }
114
135
 
115
- const saveEntry = (key: string, value: ReturnType<T>) => {
116
- cache.set(key, { value, timestamp: Date.now() })
117
- ensureCleanup()
118
- }
136
+ const isFresh = (entry: CacheStoreEntry) =>
137
+ Date.now() - entry.timestamp < cacheTimeMs
119
138
 
120
- const fetchAndPopulate = (
121
- key: string,
122
- args: Parameters<T>
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
- if (!isPromiseLike(result)) {
127
- saveEntry(key, result as ReturnType<T>)
128
- return result as ReturnType<T>
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
- const promise = (result as Promise<ReturnType<T>>)
132
- .then((value) => {
133
- saveEntry(key, value)
134
- pendingPromises.delete(key)
135
- return value
136
- })
137
- .catch((error: unknown) => {
138
- pendingPromises.delete(key)
139
- throw error
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
- pendingPromises.set(key, promise)
143
- return promise as ReturnType<T>
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
- const startBackgroundRefresh = (
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
- staleValue: ReturnType<T>
150
- ) => {
179
+ stale?: CacheStoreEntry
180
+ ): unknown => {
151
181
  const result = fn(...args)
152
182
 
153
183
  if (!isPromiseLike(result)) {
154
- saveEntry(key, result as ReturnType<T>)
155
- return
184
+ save(key, result)
185
+ return result
156
186
  }
157
187
 
158
- const promise = (result as Promise<ReturnType<T>>)
159
- .then((value) => {
160
- saveEntry(key, value)
161
- pendingPromises.delete(key)
162
- return value
163
- })
164
- .catch((error: unknown) => {
165
- pendingPromises.delete(key)
166
- onBackgroundRefreshError?.(error, { name })
167
- // Anyone awaiting this promise gets the stale value instead of
168
- // undefined or a rejection.
169
- return staleValue
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
- const saveToStore = async (
176
- cacheStore: CacheStore,
177
- key: string,
178
- value: unknown
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
- let entry: CacheStoreEntry | undefined
205
- try {
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
- const isFresh = Date.now() - entry.timestamp < cacheTimeMs
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
- const existing = pendingPromises.get(key)
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
- cache.delete(key)
252
+ if (externalStore) {
253
+ return callWithStore(externalStore, key, args) as ReturnType<T>
290
254
  }
291
255
 
292
- const pending = pendingPromises.get(key)
293
- if (pending) {
294
- return pending as ReturnType<T>
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 fetchAndPopulate(key, args)
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
- if (externalStore?.clear) {
305
- externalStore.clear().catch((error: unknown) => {
306
- onStoreError?.(error, { name })
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'