hanzio 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +510 -0
  2. package/dist/api-wrapper/index.d.ts +84 -0
  3. package/dist/api-wrapper/index.js +250 -0
  4. package/dist/array/index.d.ts +22 -0
  5. package/dist/cache/index.d.ts +19 -0
  6. package/dist/date/index.d.ts +11 -0
  7. package/dist/gzip/index.d.ts +2 -0
  8. package/dist/gzip/index.js +16 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.js +1565 -0
  11. package/dist/jwt/index.d.ts +3 -0
  12. package/dist/jwt/index.js +19 -0
  13. package/dist/math/index.d.ts +2 -0
  14. package/dist/math/linearRegressionTrend.d.ts +5 -0
  15. package/dist/math/metricsSeriesColors.d.ts +42 -0
  16. package/dist/p-queue/index.d.ts +95 -0
  17. package/dist/p-queue/index.js +439 -0
  18. package/dist/promise/index.d.ts +21 -0
  19. package/dist/sitemap/index.d.ts +14 -0
  20. package/dist/sitemap/index.js +552 -0
  21. package/dist/state/index.d.ts +101 -0
  22. package/dist/string/index.d.ts +8 -0
  23. package/dist/typedswitch/index.d.ts +124 -0
  24. package/dist/url/index.d.ts +1 -0
  25. package/dist/zod/index.d.ts +8 -0
  26. package/dist/zod/index.js +210 -0
  27. package/dist/zod/jsonToZodSchemaMapper.d.ts +6 -0
  28. package/dist/zod/zodMinMaxFilter.d.ts +14 -0
  29. package/dist/zod/zodToTypeString.d.ts +9 -0
  30. package/package.json +102 -0
  31. package/src/api-wrapper/index.ts +492 -0
  32. package/src/array/index.ts +187 -0
  33. package/src/cache/index.ts +154 -0
  34. package/src/date/index.ts +54 -0
  35. package/src/gzip/index.ts +29 -0
  36. package/src/index.ts +12 -0
  37. package/src/jwt/index.ts +25 -0
  38. package/src/math/index.ts +11 -0
  39. package/src/math/linearRegressionTrend.ts +36 -0
  40. package/src/math/metricsSeriesColors.ts +208 -0
  41. package/src/p-queue/index.ts +690 -0
  42. package/src/promise/index.ts +73 -0
  43. package/src/sitemap/index.ts +195 -0
  44. package/src/state/index.ts +331 -0
  45. package/src/string/index.ts +86 -0
  46. package/src/typedswitch/index.ts +309 -0
  47. package/src/url/index.ts +5 -0
  48. package/src/zod/index.ts +29 -0
  49. package/src/zod/jsonToZodSchemaMapper.ts +44 -0
  50. package/src/zod/zodMinMaxFilter.ts +28 -0
  51. package/src/zod/zodToTypeString.ts +222 -0
@@ -0,0 +1,154 @@
1
+ type CacheEntry<T> = {
2
+ value: T
3
+ timestamp: number
4
+ }
5
+
6
+ type AnyFunction = (...args: never[]) => unknown
7
+ type CachedFunction<T extends AnyFunction> = {
8
+ (...args: Parameters<T>): ReturnType<T>
9
+ clearCache: () => void
10
+ }
11
+
12
+ export type CacheFunctionOptions<T extends AnyFunction> = {
13
+ name: string
14
+ fn: T
15
+ cacheTimeMs?: number
16
+ refreshInBackground?: boolean
17
+ cacheKeyArgs?: number[]
18
+ cacheKeyFn?: (...args: Parameters<T>) => unknown[]
19
+ /** Called when a background refresh fails (default: silent). */
20
+ onBackgroundRefreshError?: (error: unknown, context: { name: string }) => void
21
+ }
22
+
23
+ export function cacheFunction<T extends AnyFunction>(
24
+ options: CacheFunctionOptions<T>
25
+ ): CachedFunction<T> {
26
+ const {
27
+ name,
28
+ fn,
29
+ cacheTimeMs = 10 * 60 * 1000,
30
+ refreshInBackground = false,
31
+ cacheKeyArgs,
32
+ cacheKeyFn,
33
+ onBackgroundRefreshError
34
+ } = options
35
+
36
+ const cache = new Map<string, CacheEntry<ReturnType<T>>>()
37
+ const pendingPromises = new Map<string, Promise<unknown>>()
38
+
39
+ const cleanupInterval = setInterval(
40
+ () => {
41
+ const now = Date.now()
42
+ for (const [key, entry] of cache.entries()) {
43
+ if (now - entry.timestamp >= cacheTimeMs) {
44
+ cache.delete(key)
45
+ }
46
+ }
47
+ },
48
+ Math.min(cacheTimeMs / 2, 5 * 60 * 1000)
49
+ )
50
+
51
+ const wrappedFn = (...args: Parameters<T>): ReturnType<T> => {
52
+ const keyArgs = cacheKeyFn
53
+ ? cacheKeyFn(...args)
54
+ : cacheKeyArgs
55
+ ? cacheKeyArgs.map((i) => args[i])
56
+ : args
57
+ const key = `${name}:${JSON.stringify(keyArgs)}`
58
+ const now = Date.now()
59
+
60
+ const pending = pendingPromises.get(key)
61
+ if (pending) {
62
+ return pending as ReturnType<T>
63
+ }
64
+
65
+ const cached = cache.get(key)
66
+
67
+ if (cached) {
68
+ const isCacheFresh = now - cached.timestamp < cacheTimeMs
69
+
70
+ if (!isCacheFresh && !refreshInBackground) {
71
+ cache.delete(key)
72
+ } else {
73
+ if (isCacheFresh && !refreshInBackground) {
74
+ return cached.value
75
+ }
76
+
77
+ if (refreshInBackground) {
78
+ const existingPending = pendingPromises.get(key)
79
+ if (existingPending) {
80
+ return existingPending as ReturnType<T>
81
+ }
82
+
83
+ const backgroundUpdate = fn(...args)
84
+
85
+ if (isPromiseLike(backgroundUpdate)) {
86
+ const backgroundPromise = (
87
+ backgroundUpdate as Promise<ReturnType<T>>
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
+ })
105
+ }
106
+
107
+ return cached.value
108
+ }
109
+
110
+ if (isCacheFresh) {
111
+ return cached.value
112
+ }
113
+ }
114
+ }
115
+
116
+ const result = fn(...args)
117
+
118
+ if (isPromiseLike(result)) {
119
+ const promise = (result as Promise<ReturnType<T>>)
120
+ .then((resolvedValue) => {
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
+ })
129
+
130
+ pendingPromises.set(key, promise)
131
+ return promise as ReturnType<T>
132
+ }
133
+
134
+ cache.set(key, { value: result as ReturnType<T>, timestamp: now })
135
+ return result as ReturnType<T>
136
+ }
137
+
138
+ wrappedFn.clearCache = () => {
139
+ cache.clear()
140
+ pendingPromises.clear()
141
+ clearInterval(cleanupInterval)
142
+ }
143
+
144
+ return wrappedFn
145
+ }
146
+
147
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
148
+ return (
149
+ typeof value === 'object' &&
150
+ value !== null &&
151
+ 'then' in value &&
152
+ typeof value.then === 'function'
153
+ )
154
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Calculate the previous period date range for a given period type.
3
+ */
4
+ export function getPreviousReportPeriod(
5
+ periodType: 'MONTHLY' | 'QUARTERLY' | 'YEARLY',
6
+ referenceDate: Date = new Date()
7
+ ): { start: Date; end: Date } {
8
+ const month = referenceDate.getMonth()
9
+ const year = referenceDate.getFullYear()
10
+
11
+ switch (periodType) {
12
+ case 'MONTHLY': {
13
+ const prevMonth = month === 0 ? 11 : month - 1
14
+ const prevYear = month === 0 ? year - 1 : year
15
+ return {
16
+ start: new Date(prevYear, prevMonth, 1),
17
+ end: new Date(year, month, 0)
18
+ }
19
+ }
20
+ case 'QUARTERLY': {
21
+ const qStart = Math.floor(month / 3) * 3
22
+ const prevQStart = qStart === 0 ? 9 : qStart - 3
23
+ const prevQYear = qStart === 0 ? year - 1 : year
24
+ return {
25
+ start: new Date(prevQYear, prevQStart, 1),
26
+ end: new Date(prevQYear, prevQStart + 3, 0)
27
+ }
28
+ }
29
+ case 'YEARLY': {
30
+ return {
31
+ start: new Date(year - 1, 0, 1),
32
+ end: new Date(year - 1, 11, 31)
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Format a date range as a human-readable period label.
40
+ */
41
+ export function formatPeriodLabel(
42
+ start: Date | string,
43
+ end: Date | string,
44
+ includeDay = false
45
+ ): string {
46
+ const s = start instanceof Date ? start : new Date(start)
47
+ const e = end instanceof Date ? end : new Date(end)
48
+ const opts: Intl.DateTimeFormatOptions = {
49
+ year: 'numeric',
50
+ month: 'long',
51
+ ...(includeDay && { day: 'numeric' })
52
+ }
53
+ return `${s.toLocaleDateString('en-GB', opts)} — ${e.toLocaleDateString('en-GB', opts)}`
54
+ }
@@ -0,0 +1,29 @@
1
+ import pako from 'pako'
2
+
3
+ export function compressString(input: string): string {
4
+ // Convert string to Uint8Array
5
+ const uint8Array = new TextEncoder().encode(input)
6
+
7
+ // Compress the Uint8Array
8
+ const compressed = pako.deflate(uint8Array)
9
+
10
+ // Convert compressed Uint8Array to base64 string for storage
11
+ return btoa(
12
+ String.fromCharCode.apply(null, compressed as unknown as number[])
13
+ )
14
+ }
15
+
16
+ export function decompressString(compressedString: string): string {
17
+ // Convert base64 string back to Uint8Array
18
+ const compressedData = new Uint8Array(
19
+ atob(compressedString)
20
+ .split('')
21
+ .map((char) => char.charCodeAt(0))
22
+ )
23
+
24
+ // Decompress the Uint8Array
25
+ const decompressed = pako.inflate(compressedData)
26
+
27
+ // Convert Uint8Array back to string
28
+ return new TextDecoder().decode(decompressed)
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from './typedswitch'
2
+ export * from './array'
3
+ export * from './string'
4
+ export * from './promise'
5
+ export * from './cache'
6
+ export * from './state'
7
+ export * from './math'
8
+ export * from './date'
9
+ export * from './url'
10
+ export * from './zod'
11
+ export * from './api-wrapper'
12
+ export * from './p-queue'
@@ -0,0 +1,25 @@
1
+ import * as jose from 'jose'
2
+
3
+ export async function jwtSign<_Payload extends object>(
4
+ payload: jose.JWTPayload,
5
+ secret: string
6
+ ) {
7
+ const secretKey = new TextEncoder().encode(secret)
8
+ return await new jose.SignJWT(payload)
9
+ .setProtectedHeader({ alg: 'HS256' })
10
+ .setIssuedAt()
11
+ .sign(secretKey)
12
+ }
13
+
14
+ export async function jwtVerify<Payload extends object>(
15
+ token: string,
16
+ secret: string
17
+ ): Promise<Payload | null> {
18
+ try {
19
+ const secretKey = new TextEncoder().encode(secret)
20
+ const { payload } = await jose.jwtVerify(token, secretKey)
21
+ return payload as Payload
22
+ } catch (_error) {
23
+ return null
24
+ }
25
+ }
@@ -0,0 +1,11 @@
1
+ export { linearRegressionTrend } from './linearRegressionTrend'
2
+ export {
3
+ createResolveColorsByKeys,
4
+ getHeatmapCellStyle,
5
+ hashStringToColorIndex,
6
+ METRICS_SERIES_PALETTE_SIZE,
7
+ type ResolveColorsByKeysResult,
8
+ resolveSeriesColorMap,
9
+ seriesColorKey,
10
+ withAlpha
11
+ } from './metricsSeriesColors'
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Least-squares linear regression for y at x = 0, 1, …, n − 1.
3
+ * Returns fitted y for each index, or [] if n < 2 or any input is non-finite.
4
+ */
5
+ export function linearRegressionTrend(values: number[]): number[] {
6
+ const n = values.length
7
+ if (n < 2) return []
8
+ for (const v of values) {
9
+ if (!Number.isFinite(v)) return []
10
+ }
11
+
12
+ let sumX = 0
13
+ let sumY = 0
14
+ let sumXY = 0
15
+ let sumX2 = 0
16
+ for (let i = 0; i < n; i++) {
17
+ const x = i
18
+ const y = values[i]!
19
+ sumX += x
20
+ sumY += y
21
+ sumXY += x * y
22
+ sumX2 += x * x
23
+ }
24
+
25
+ const denom = n * sumX2 - sumX * sumX
26
+ if (denom === 0) return []
27
+
28
+ const slope = (n * sumXY - sumX * sumY) / denom
29
+ const intercept = (sumY - slope * sumX) / n
30
+
31
+ const out: number[] = []
32
+ for (let i = 0; i < n; i++) {
33
+ out.push(intercept + slope * i)
34
+ }
35
+ return out
36
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Shared metrics chart series color assignment for web (CSS tokens) and Expo (hex palette).
3
+ * Same hash + collision strategy so labels map to the same slot index everywhere.
4
+ */
5
+
6
+ /** Matches PrimeVue semantic palette slot count (cyan…blue) and Expo SERIES_COLORS length */
7
+ export const METRICS_SERIES_PALETTE_SIZE = 10
8
+
9
+ /**
10
+ * FNV-1a 32-bit hash — stable palette index for a string key.
11
+ */
12
+ export function hashStringToColorIndex(str: string): number {
13
+ let hash = 2166136261
14
+ for (let i = 0; i < str.length; i++) {
15
+ hash ^= str.charCodeAt(i)
16
+ hash = Math.imul(hash, 16777619)
17
+ }
18
+ return Math.abs(hash) % METRICS_SERIES_PALETTE_SIZE
19
+ }
20
+
21
+ function circularHueDistance(
22
+ a: number,
23
+ b: number,
24
+ paletteSize: number
25
+ ): number {
26
+ const diff = Math.abs(a - b)
27
+ return Math.min(diff, paletteSize - diff)
28
+ }
29
+
30
+ function pickHueMaxMinDistance(
31
+ candidates: number[],
32
+ used: Set<number>,
33
+ paletteSize: number
34
+ ): number {
35
+ let best = candidates[0] ?? 0
36
+ let bestScore = -1
37
+ for (const cand of candidates) {
38
+ let minD = paletteSize
39
+ for (const u of used) {
40
+ minD = Math.min(minD, circularHueDistance(cand, u, paletteSize))
41
+ }
42
+ if (minD > bestScore || (minD === bestScore && cand < best)) {
43
+ bestScore = minD
44
+ best = cand
45
+ }
46
+ }
47
+ return best
48
+ }
49
+
50
+ /**
51
+ * Prefer human-visible label so charts and tables agree on the same color key.
52
+ */
53
+ export function seriesColorKey(label: string | undefined, key: string): string {
54
+ const fromLabel = label?.trim()
55
+ if (fromLabel) return fromLabel
56
+ return key.trim() || key
57
+ }
58
+
59
+ /**
60
+ * Map each distinct key to a palette index (hash preference, then max-min circular spacing).
61
+ */
62
+ export function resolveSeriesColorMap(keys: string[]): Map<string, number> {
63
+ const result = new Map<string, number>()
64
+ const uniqueKeys = [...new Set(keys)]
65
+ if (uniqueKeys.length === 0) return result
66
+
67
+ const paletteSize = METRICS_SERIES_PALETTE_SIZE
68
+
69
+ if (uniqueKeys.length === 1) {
70
+ const only = uniqueKeys[0]!
71
+ result.set(only, hashStringToColorIndex(only))
72
+ return result
73
+ }
74
+
75
+ const usedIndices = new Set<number>()
76
+ const entries = uniqueKeys.map((key) => ({
77
+ key,
78
+ preferred: hashStringToColorIndex(key)
79
+ }))
80
+
81
+ for (const entry of entries) {
82
+ if (!usedIndices.has(entry.preferred)) {
83
+ result.set(entry.key, entry.preferred)
84
+ usedIndices.add(entry.preferred)
85
+ }
86
+ }
87
+
88
+ const unassigned = entries.filter((e) => !result.has(e.key))
89
+ for (const entry of unassigned) {
90
+ const unused: number[] = []
91
+ for (let i = 0; i < paletteSize; i++) {
92
+ if (!usedIndices.has(i)) unused.push(i)
93
+ }
94
+ const candidates =
95
+ unused.length > 0
96
+ ? unused
97
+ : Array.from({ length: paletteSize }, (_, i) => i)
98
+ const idx = pickHueMaxMinDistance(candidates, usedIndices, paletteSize)
99
+ result.set(entry.key, idx)
100
+ usedIndices.add(idx)
101
+ }
102
+
103
+ return result
104
+ }
105
+
106
+ export type ResolveColorsByKeysResult = {
107
+ getColor: (key: string) => string
108
+ getHoverColor: (key: string) => string
109
+ }
110
+
111
+ /**
112
+ * Resolve stable colors per key using palette index → RGB/CSS from the caller.
113
+ */
114
+ export function createResolveColorsByKeys(
115
+ keys: string[],
116
+ getSeriesColor: (index: number) => string,
117
+ getSeriesHoverColor: (index: number) => string
118
+ ): ResolveColorsByKeysResult {
119
+ const colorMap = resolveSeriesColorMap(keys)
120
+ return {
121
+ getColor: (key: string) =>
122
+ getSeriesColor(colorMap.get(key) ?? hashStringToColorIndex(key)),
123
+ getHoverColor: (key: string) =>
124
+ getSeriesHoverColor(colorMap.get(key) ?? hashStringToColorIndex(key))
125
+ }
126
+ }
127
+
128
+ function clamp(value: number, min: number, max: number): number {
129
+ return Math.min(max, Math.max(min, value))
130
+ }
131
+
132
+ /**
133
+ * Add alpha to hex, rgb(), or rgba() strings.
134
+ */
135
+ export function withAlpha(color: string, alpha: number): string {
136
+ const trimmed = color.trim()
137
+
138
+ if (trimmed.startsWith('#')) {
139
+ const hex = trimmed.slice(1)
140
+ const normalized =
141
+ hex.length === 3
142
+ ? hex
143
+ .split('')
144
+ .map((c) => c + c)
145
+ .join('')
146
+ : hex
147
+ if (normalized.length === 6) {
148
+ const r = Number.parseInt(normalized.slice(0, 2), 16)
149
+ const g = Number.parseInt(normalized.slice(2, 4), 16)
150
+ const b = Number.parseInt(normalized.slice(4, 6), 16)
151
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`
152
+ }
153
+ }
154
+
155
+ if (trimmed.startsWith('rgb(')) {
156
+ return trimmed.replace('rgb(', 'rgba(').replace(')', `, ${alpha})`)
157
+ }
158
+
159
+ if (trimmed.startsWith('rgba(')) {
160
+ return trimmed.replace(/,\s*[\d.]+\)$/, `, ${alpha})`)
161
+ }
162
+
163
+ return trimmed
164
+ }
165
+
166
+ /**
167
+ * Heatmap cell background from normalized value intensity.
168
+ */
169
+ export function getHeatmapCellStyle(params: {
170
+ value: number | null | undefined
171
+ min: number
172
+ max: number
173
+ baseColor: string
174
+ neutralColor?: string
175
+ minAlpha?: number
176
+ maxAlpha?: number
177
+ }): Record<string, string> {
178
+ const {
179
+ value,
180
+ min,
181
+ max,
182
+ baseColor,
183
+ neutralColor,
184
+ minAlpha = 0.08,
185
+ maxAlpha = 0.78
186
+ } = params
187
+
188
+ if (value === null || value === undefined || Number.isNaN(value)) {
189
+ return {}
190
+ }
191
+
192
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
193
+ return {}
194
+ }
195
+
196
+ if (max <= min) {
197
+ return {
198
+ backgroundColor: withAlpha(neutralColor ?? baseColor, 0.32)
199
+ }
200
+ }
201
+
202
+ const normalized = clamp((value - min) / (max - min), 0, 1)
203
+ const alpha = minAlpha + (maxAlpha - minAlpha) * normalized
204
+
205
+ return {
206
+ backgroundColor: withAlpha(baseColor, alpha)
207
+ }
208
+ }