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
package/README.md ADDED
@@ -0,0 +1,510 @@
1
+ # hanzio
2
+
3
+ A small TypeScript-first utility library for common application code: arrays,
4
+ strings, promises, caching, state machines, dates, URLs, Zod helpers, API clients,
5
+ JWTs, gzip, sitemap crawling, and queues.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add hanzio
11
+ ```
12
+
13
+ The package is ESM and Bun-friendly. Bun consumers resolve the `bun` export to
14
+ the source TypeScript files, while Node/bundler consumers resolve built files
15
+ from `dist`.
16
+
17
+ ## Imports
18
+
19
+ Most lightweight utilities are available from the root export:
20
+
21
+ ```ts
22
+ import { groupBy, typedSwitch, tryCatch } from 'hanzio'
23
+ ```
24
+
25
+ Heavier or dependency-specific modules are also available as subpath exports:
26
+
27
+ ```ts
28
+ import { createZId } from 'hanzio/zod'
29
+ import { jwtSign } from 'hanzio/jwt'
30
+ import { compressString } from 'hanzio/gzip'
31
+ import { getDomainSitemap } from 'hanzio/sitemap'
32
+ import PQueue from 'hanzio/p-queue'
33
+ ```
34
+
35
+ The package currently publishes `src/**/*.ts` intentionally so Bun can consume
36
+ the source entrypoints directly. Tests are excluded from the published package.
37
+
38
+ ## Utilities
39
+
40
+ ### Array
41
+
42
+ `chunkArray` splits a readonly array into fixed-size chunks.
43
+
44
+ ```ts
45
+ import { chunkArray } from 'hanzio'
46
+
47
+ const pages = chunkArray([1, 2, 3, 4, 5], 2)
48
+ // [[1, 2], [3, 4], [5]]
49
+ ```
50
+
51
+ `findArrayDifferenceByKey` compares two arrays by a shared key and returns
52
+ `new`, `upsert`, and `delete` groups.
53
+
54
+ ```ts
55
+ import { findArrayDifferenceByKey } from 'hanzio'
56
+
57
+ const diff = findArrayDifferenceByKey(
58
+ [{ id: 1, name: 'old' }],
59
+ [{ id: 1, name: 'new' }, { id: 2, name: 'added' }],
60
+ 'id'
61
+ )
62
+ ```
63
+
64
+ `getUniqueValues` keeps the first unique serialized value.
65
+
66
+ ```ts
67
+ import { getUniqueValues } from 'hanzio'
68
+
69
+ const values = getUniqueValues([{ id: 1 }, { id: 1 }, { id: 2 }])
70
+ // [{ id: 1 }, { id: 2 }]
71
+ ```
72
+
73
+ `compact` removes `null` and `undefined` while narrowing the returned type.
74
+
75
+ ```ts
76
+ import { compact } from 'hanzio'
77
+
78
+ const names = compact(['Ada', null, 'Grace', undefined])
79
+ // string[]
80
+ ```
81
+
82
+ `getUniqueValuesByKey` keeps the first item for each key value.
83
+
84
+ ```ts
85
+ import { getUniqueValuesByKey } from 'hanzio'
86
+
87
+ const users = getUniqueValuesByKey(
88
+ [{ id: 'a', role: 'admin' }, { id: 'a', role: 'user' }],
89
+ 'id'
90
+ )
91
+ ```
92
+
93
+ `groupBy` groups items by a property or callback and preserves literal key unions
94
+ when possible.
95
+
96
+ ```ts
97
+ import { groupBy } from 'hanzio'
98
+
99
+ const byType = groupBy(
100
+ [
101
+ { type: 'fruit', name: 'apple' },
102
+ { type: 'veg', name: 'carrot' }
103
+ ] as const,
104
+ 'type'
105
+ )
106
+ ```
107
+
108
+ `keyBy` indexes items by a property or callback.
109
+
110
+ ```ts
111
+ import { keyBy } from 'hanzio'
112
+
113
+ const byId = keyBy([{ id: 'user_1', name: 'Ada' }], 'id')
114
+ // { user_1: { id: 'user_1', name: 'Ada' } }
115
+ ```
116
+
117
+ `partition` splits an array into matched and unmatched items.
118
+
119
+ ```ts
120
+ import { partition } from 'hanzio'
121
+
122
+ const [even, odd] = partition([1, 2, 3, 4], (value) => value % 2 === 0)
123
+ ```
124
+
125
+ `pickItemsInArray` filters by a key and narrows discriminated unions for const
126
+ value lists.
127
+
128
+ ```ts
129
+ import { pickItemsInArray } from 'hanzio'
130
+
131
+ type Item = { type: 'user'; name: string } | { type: 'team'; members: number }
132
+ const users = pickItemsInArray([] as Item[], 'type', ['user'] as const)
133
+ // Array<{ type: 'user'; name: string }>
134
+ ```
135
+
136
+ ### String
137
+
138
+ `extractNumber` parses the first number-like value from a string.
139
+
140
+ ```ts
141
+ import { extractNumber } from 'hanzio'
142
+
143
+ const amount = extractNumber('Total: 123.45 kr')
144
+ // 123.45
145
+ ```
146
+
147
+ `generateId` creates a stable short ID from JSON-like input.
148
+
149
+ ```ts
150
+ import { generateId } from 'hanzio'
151
+
152
+ const id = generateId({ page: 1, filters: ['active'] })
153
+ ```
154
+
155
+ `slugify` normalizes text for URL slugs.
156
+
157
+ ```ts
158
+ import { slugify } from 'hanzio'
159
+
160
+ const slug = slugify('Hello, København!')
161
+ // "hello-kobenhavn"
162
+ ```
163
+
164
+ ### Promise
165
+
166
+ `tryCatch` converts a promise into a `Result<T, E>` object.
167
+
168
+ ```ts
169
+ import { tryCatch } from 'hanzio'
170
+
171
+ const result = await tryCatch(fetch('/api/users'))
172
+ ```
173
+
174
+ `isSuccess`, `isFailure`, and `unwrapResult` help narrow and consume
175
+ `Result<T, E>`.
176
+
177
+ ```ts
178
+ import { isSuccess, unwrapResult } from 'hanzio'
179
+
180
+ if (isSuccess(result)) {
181
+ result.data
182
+ }
183
+
184
+ const response = unwrapResult(result)
185
+ ```
186
+
187
+ `backgroundPromise` runs async work and reports success or failure through hooks.
188
+
189
+ ```ts
190
+ import { backgroundPromise } from 'hanzio'
191
+
192
+ backgroundPromise(() => syncAnalytics(), {
193
+ onError: (error) => console.error(error)
194
+ })
195
+ ```
196
+
197
+ `backgroundPromiseSync` schedules background work in a microtask by default.
198
+
199
+ ```ts
200
+ import { backgroundPromiseSync } from 'hanzio'
201
+
202
+ backgroundPromiseSync(() => refreshCache())
203
+ ```
204
+
205
+ `promiseTimeout` resolves after a delay.
206
+
207
+ ```ts
208
+ import { promiseTimeout } from 'hanzio'
209
+
210
+ await promiseTimeout(250)
211
+ ```
212
+
213
+ ### Cache
214
+
215
+ `cacheFunction` wraps sync or async functions with TTL caching and request
216
+ deduplication.
217
+
218
+ ```ts
219
+ import { cacheFunction } from 'hanzio'
220
+
221
+ const getUser = cacheFunction({
222
+ name: 'getUser',
223
+ fn: async (id: string) => ({ id, name: 'Ada' }),
224
+ cacheTimeMs: 60_000
225
+ })
226
+
227
+ const user = await getUser('user_1')
228
+ getUser.clearCache()
229
+ ```
230
+
231
+ ### State Machine
232
+
233
+ `SimpleStateMachine` models typed states, actions, validation, and action
234
+ payloads.
235
+
236
+ ```ts
237
+ import { SimpleStateMachine } from 'hanzio'
238
+
239
+ type State = 'draft' | 'published'
240
+ type Action = 'publish'
241
+
242
+ const machine = new SimpleStateMachine<
243
+ State,
244
+ Action,
245
+ { status: State },
246
+ { publish: { notify: boolean } }
247
+ >(
248
+ {
249
+ stateActionMap: { draft: { publish: 'published' }, published: {} },
250
+ stateTransitions: { draft: ['published'], published: [] }
251
+ },
252
+ { status: 'draft' },
253
+ 'status'
254
+ )
255
+
256
+ const result = await machine.executeAction('publish', { notify: true })
257
+ ```
258
+
259
+ ### typedSwitch
260
+
261
+ `typedSwitch` is an exhaustive switch helper for string unions and discriminated
262
+ unions.
263
+
264
+ ```ts
265
+ import { typedSwitch } from 'hanzio'
266
+
267
+ type Event =
268
+ | { type: 'click'; x: number; y: number }
269
+ | { type: 'scroll'; offset: number }
270
+
271
+ const label = typedSwitch({ type: 'click', x: 10, y: 20 } as Event, 'type', {
272
+ click: (event) => `Clicked ${event.x},${event.y}`,
273
+ scroll: (event) => `Scrolled ${event.offset}`
274
+ })
275
+ ```
276
+
277
+ ### Date
278
+
279
+ `getPreviousReportPeriod` returns the previous monthly, quarterly, or yearly
280
+ date range.
281
+
282
+ ```ts
283
+ import { getPreviousReportPeriod } from 'hanzio'
284
+
285
+ const previousMonth = getPreviousReportPeriod('MONTHLY', new Date('2026-04-15'))
286
+ ```
287
+
288
+ `formatPeriodLabel` formats a date range for display.
289
+
290
+ ```ts
291
+ import { formatPeriodLabel } from 'hanzio'
292
+
293
+ const label = formatPeriodLabel('2026-01-01', '2026-03-31')
294
+ ```
295
+
296
+ ### URL
297
+
298
+ `getDomainFaviconUrl` builds a Google favicon URL for a domain.
299
+
300
+ ```ts
301
+ import { getDomainFaviconUrl } from 'hanzio'
302
+
303
+ const favicon = getDomainFaviconUrl('example.com', 64)
304
+ ```
305
+
306
+ ### Math
307
+
308
+ `linearRegressionTrend` returns fitted trend values for a numeric series.
309
+
310
+ ```ts
311
+ import { linearRegressionTrend } from 'hanzio'
312
+
313
+ const trend = linearRegressionTrend([1, 3, 5])
314
+ // [1, 3, 5]
315
+ ```
316
+
317
+ `hashStringToColorIndex`, `seriesColorKey`, `resolveSeriesColorMap`,
318
+ `createResolveColorsByKeys`, `withAlpha`, and `getHeatmapCellStyle` help assign
319
+ stable chart colors and heatmap styles.
320
+
321
+ ```ts
322
+ import { createResolveColorsByKeys, withAlpha } from 'hanzio'
323
+
324
+ const colors = createResolveColorsByKeys(
325
+ ['revenue', 'profit'],
326
+ (index) => ['#2563eb', '#16a34a'][index] ?? '#64748b',
327
+ (index) => ['#1d4ed8', '#15803d'][index] ?? '#475569'
328
+ )
329
+ const faded = withAlpha('#2563eb', 0.4)
330
+ ```
331
+
332
+ ### Zod
333
+
334
+ `createZId` and `GenericId` create branded string ID schemas.
335
+
336
+ ```ts
337
+ import { createZId, type GenericId } from 'hanzio/zod'
338
+
339
+ const UserId = createZId('User')
340
+ type UserId = GenericId<'User'>
341
+
342
+ const userId: UserId = UserId.parse('user_123')
343
+ ```
344
+
345
+ `zSingleOrArray` accepts either one schema value or an array of schema values.
346
+
347
+ ```ts
348
+ import { zSingleOrArray } from 'hanzio/zod'
349
+ import { z } from 'zod'
350
+
351
+ const Tags = zSingleOrArray(z.string())
352
+ Tags.parse('news')
353
+ Tags.parse(['news', 'product'])
354
+ ```
355
+
356
+ `normalizeSingleOrArray` normalizes nullable single-or-array inputs to an array.
357
+
358
+ ```ts
359
+ import { normalizeSingleOrArray } from 'hanzio/zod'
360
+
361
+ const values = normalizeSingleOrArray('tag')
362
+ // ['tag']
363
+ ```
364
+
365
+ `zodMinMaxFilter` and `toGteLteFilter` model common min/max filters.
366
+
367
+ ```ts
368
+ import { toGteLteFilter, zodMinMaxFilter } from 'hanzio/zod'
369
+
370
+ const filter = zodMinMaxFilter.parse({ min: 10, max: 20 })
371
+ const query = toGteLteFilter(filter)
372
+ // { gte: 10, lte: 20 }
373
+ ```
374
+
375
+ `jsonToZodSchemaMapper` maps raw JSON into a Zod schema shape with optional field
376
+ mapping.
377
+
378
+ ```ts
379
+ import { jsonToZodSchemaMapper } from 'hanzio/zod'
380
+ import { z } from 'zod'
381
+
382
+ const User = z.object({ id: z.string(), name: z.string() })
383
+ const user = jsonToZodSchemaMapper({ user_id: '1', name: 'Ada' }, User, {
384
+ id: (json) => json.user_id
385
+ })
386
+ ```
387
+
388
+ `zodToTypeString` and `zodToExample` render simple documentation artifacts from
389
+ Zod schemas.
390
+
391
+ ```ts
392
+ import { zodToExample, zodToTypeString } from 'hanzio/zod'
393
+ import { z } from 'zod'
394
+
395
+ const schema = z.object({ id: z.string() })
396
+ const typeString = zodToTypeString(schema)
397
+ const example = zodToExample(schema)
398
+ ```
399
+
400
+ ### API Wrapper
401
+
402
+ `createApiClient` and `createApiWrapper` create a Zod-backed API client with
403
+ typed request inputs and response data.
404
+
405
+ ```ts
406
+ import { createApiClient } from 'hanzio/api-wrapper'
407
+ import { z } from 'zod'
408
+
409
+ const api = createApiClient({
410
+ baseApiUrls: { default: 'https://api.example.com' },
411
+ endpoints: {
412
+ getUser: {
413
+ method: 'GET',
414
+ path: '/users/:id',
415
+ reqParamsSchema: z.object({ id: z.number() }),
416
+ resSchema: z.object({ id: z.number(), name: z.string() })
417
+ }
418
+ }
419
+ })
420
+
421
+ const result = await api.request('getUser', { reqParams: { id: 1 } })
422
+ ```
423
+
424
+ `ResponseValidationError` and `HttpResponseError` are thrown for invalid
425
+ responses and non-OK HTTP statuses.
426
+
427
+ ```ts
428
+ import { HttpResponseError, ResponseValidationError } from 'hanzio/api-wrapper'
429
+
430
+ try {
431
+ await api.request('getUser', { reqParams: { id: 1 } })
432
+ } catch (error) {
433
+ if (error instanceof ResponseValidationError) {
434
+ console.error(error.validationIssues)
435
+ }
436
+ if (error instanceof HttpResponseError) console.error(error.status)
437
+ }
438
+ ```
439
+
440
+ ### JWT
441
+
442
+ `jwtSign` signs a payload with HS256, and `jwtVerify` verifies a token or returns
443
+ `null`.
444
+
445
+ ```ts
446
+ import { jwtSign, jwtVerify } from 'hanzio/jwt'
447
+
448
+ const token = await jwtSign({ sub: 'user_1' }, 'secret')
449
+ const payload = await jwtVerify<{ sub: string }>(token, 'secret')
450
+ ```
451
+
452
+ ### Gzip
453
+
454
+ `compressString` and `decompressString` gzip roundtrip strings as base64.
455
+
456
+ ```ts
457
+ import { compressString, decompressString } from 'hanzio/gzip'
458
+
459
+ const compressed = compressString('hello')
460
+ const text = decompressString(compressed)
461
+ ```
462
+
463
+ ### Sitemap
464
+
465
+ `getDomainSitemap` discovers and crawls sitemap URLs for a domain.
466
+
467
+ ```ts
468
+ import { getDomainSitemap } from 'hanzio/sitemap'
469
+
470
+ const urls = await getDomainSitemap('example.com', {
471
+ concurrency: 5,
472
+ maxDepth: 3
473
+ })
474
+ ```
475
+
476
+ ### PQueue
477
+
478
+ `PQueue` runs promise-returning tasks with concurrency, priority, timeout, abort,
479
+ and rate-limit controls.
480
+
481
+ ```ts
482
+ import PQueue from 'hanzio/p-queue'
483
+
484
+ const queue = new PQueue({ concurrency: 2 })
485
+
486
+ const result = await queue.add(async () => 'done')
487
+ await queue.onIdle()
488
+ ```
489
+
490
+ `PriorityQueue` and `TimeoutError` are exported for custom queue behavior and
491
+ timeout handling.
492
+
493
+ ```ts
494
+ import PQueue, { TimeoutError } from 'hanzio/p-queue'
495
+
496
+ try {
497
+ await new PQueue({ timeout: 100 }).add(() => new Promise(() => {}))
498
+ } catch (error) {
499
+ if (error instanceof TimeoutError) console.error('Task timed out')
500
+ }
501
+ ```
502
+
503
+ ## Development
504
+
505
+ ```bash
506
+ bun run typecheck
507
+ bun test
508
+ bun run lint
509
+ bun run build
510
+ ```
@@ -0,0 +1,84 @@
1
+ import { type z, ZodError } from 'zod';
2
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
3
+ export type RequestBodyFormat = 'json' | 'form-data';
4
+ export type BaseApiUrl = string | (() => string);
5
+ export type PathParams = Record<string, string | number>;
6
+ export type QueryParams = Record<string, string | number | boolean | undefined>;
7
+ export declare class ResponseValidationError extends Error {
8
+ readonly zodError: ZodError;
9
+ readonly rawResponse: unknown;
10
+ readonly validationIssues: string;
11
+ constructor(zodError: ZodError, rawResponse: unknown);
12
+ }
13
+ export declare class HttpResponseError extends Error {
14
+ readonly status: number;
15
+ readonly body: string;
16
+ constructor(status: number, body: string);
17
+ }
18
+ export interface ApiEndpoint<TReqBody extends z.ZodType | undefined = z.ZodType | undefined, TReqParams extends z.ZodType | undefined = z.ZodType | undefined, TReqQuery extends z.ZodType | undefined = z.ZodType | undefined, TReqHeaders extends z.ZodType | undefined = z.ZodType | undefined, TResponse extends z.ZodType = z.ZodType> {
19
+ method: HttpMethod;
20
+ path: string | ((baseUrl: string) => string);
21
+ reqBodySchema?: TReqBody;
22
+ reqParamsSchema?: TReqParams;
23
+ reqQuerySchema?: TReqQuery;
24
+ reqHeadersSchema?: TReqHeaders;
25
+ resSchema: TResponse;
26
+ baseApiUrl?: string;
27
+ reqBodyFormat?: RequestBodyFormat;
28
+ defaultHeaders?: Record<string, string>;
29
+ resFormatter?: (data: unknown, headers: Record<string, string>) => z.infer<TResponse>;
30
+ reqDefaultQueryParams?: QueryParams;
31
+ doNotEncodeQueryParams?: boolean;
32
+ }
33
+ export interface ApiWrapperResponse<T> {
34
+ data: T;
35
+ requestSizeMb: number;
36
+ responseSizeMb: number;
37
+ requestSizeTotalMb: number;
38
+ responseTimeMs: number;
39
+ httpStatus: number;
40
+ retryCount: number;
41
+ }
42
+ export type RetryContext = {
43
+ error?: unknown;
44
+ response?: Response;
45
+ retryCount: number;
46
+ maxRetries: number;
47
+ };
48
+ export interface ApiClientConfig<T extends Record<string, ApiEndpoint>> {
49
+ name?: string;
50
+ baseApiUrls: Record<string, BaseApiUrl>;
51
+ defaultBaseApiUrl?: string;
52
+ endpoints: T;
53
+ defaultHeaders?: Record<string, string>;
54
+ timeoutMs?: number;
55
+ retries?: number;
56
+ retryDelayMs?: number;
57
+ shouldRetry?: (context: RetryContext) => boolean;
58
+ logger?: Pick<Console, 'debug' | 'error'>;
59
+ fetch?: typeof fetch;
60
+ onRequestStart?: (context: {
61
+ endpoint: string;
62
+ url: string;
63
+ }) => void;
64
+ onRequestEnd?: (context: {
65
+ endpoint: string;
66
+ url: string;
67
+ status: number;
68
+ retryCount: number;
69
+ responseTimeMs: number;
70
+ }) => void;
71
+ }
72
+ type RequestInput<TEndpoint extends ApiEndpoint> = {
73
+ reqBody?: InferOptionalSchema<TEndpoint['reqBodySchema']>;
74
+ reqParams?: InferOptionalSchema<TEndpoint['reqParamsSchema']>;
75
+ reqQuery?: InferOptionalSchema<TEndpoint['reqQuerySchema']>;
76
+ reqHeaders?: InferOptionalSchema<TEndpoint['reqHeadersSchema']>;
77
+ url?: string;
78
+ };
79
+ type InferOptionalSchema<TSchema> = TSchema extends z.ZodType ? z.infer<TSchema> : undefined;
80
+ export declare function createApiClient<T extends Record<string, ApiEndpoint>>(apiConfig: ApiClientConfig<T>): {
81
+ request: <K extends keyof T>(endpointKey: K, input?: RequestInput<T[K]>) => Promise<ApiWrapperResponse<z.TypeOf<T[K]["resSchema"]>>>;
82
+ };
83
+ export declare const createApiWrapper: typeof createApiClient;
84
+ export {};