hanzio 2.1.0 → 2.2.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.
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: Discover and crawl XML sitemaps for URL discovery; uses the local p-queue utility for concurrency.
3
+ ---
4
+
5
+ # hanzio/sitemap
6
+
7
+ `getDomainSitemap(domain, options)` finds a domain's sitemaps (the `Sitemap:` lines in robots.txt first, then common locations like `/sitemap.xml`) and crawls them with bounded parallelism using the local `hanzio/p-queue` utility.
8
+
9
+ - A document whose root element is `<sitemapindex>` is treated as a list of child sitemaps. A document whose root element is `<urlset>` is treated as a list of pages.
10
+ - `<loc>` values are XML-entity decoded, and CDATA is supported.
11
+ - Gzipped sitemaps (`.xml.gz`) are decompressed with `DecompressionStream`.
12
+ - Every request has its own timeout (`AbortSignal.timeout`).
13
+
14
+ Requires runtime `fetch`, `AbortSignal.timeout` and `DecompressionStream` (Node 18+, Bun, modern browsers), or pass your own `fetch`.
15
+
16
+ ```ts
17
+ import { getDomainSitemap, getDomainSitemapEntries } from 'hanzio/sitemap'
18
+
19
+ const urls = await getDomainSitemap('example.com', {
20
+ filterIndexes: 'posts', // substring, RegExp or (url) => boolean
21
+ filterUrls: /\/blog\//,
22
+ maxUrls: 1000
23
+ })
24
+
25
+ // With lastmod
26
+ const entries = await getDomainSitemapEntries('example.com')
27
+ // [{ loc: 'https://example.com/a', lastmod: '2024-01-05' }, ...]
28
+ ```
29
+
30
+ ## Options (`SitemapOptions`)
31
+
32
+ | Option | Default | Description |
33
+ | --- | --- | --- |
34
+ | `filterIndexes` | none | Only follow child sitemaps that match (substring, RegExp or predicate). |
35
+ | `filterUrls` | none | Only keep page URLs that match. |
36
+ | `concurrency` | `10` | The maximum number of sitemap fetches that run at the same time. |
37
+ | `maxDepth` | `5` | The maximum sitemap-index nesting depth. |
38
+ | `maxUrls` | none | Stop crawling after this many URLs have been collected. |
39
+ | `timeout` | `10000` | The timeout for each request, in ms. |
40
+ | `userAgent` | `'SitemapCrawler/1.0'` | The `User-Agent` header sent with each request. |
41
+ | `fetch` | `globalThis.fetch` | A custom fetch implementation. |
42
+ | `onError` | `console.warn` | `(url, error) => void`, called when a robots.txt or sitemap request fails. |
43
+ | `respectRobots` | `false` | Drop page URLs that robots.txt disallows for `userAgent` (reuses the robots.txt fetched for discovery). A missing or failed robots.txt allows everything. |
44
+
45
+ Both functions throw `No sitemaps found for the domain` when discovery finds nothing.
46
+
47
+ ## robots.txt
48
+
49
+ `parseRobotsTxt(body, userAgent = '*')` returns the rules for one crawler, following RFC 9309: the groups that name its product token (case-insensitive, so `MyBot/2.0` matches `User-agent: mybot`), merged, or else the `User-agent: *` groups. Directive names are case-insensitive and `#` comments are ignored. `sitemaps` lists every `Sitemap:` line.
50
+
51
+ `isAllowedByRobots(rules, url)` checks an absolute URL or a path (plus query string). The longest matching pattern wins, `Allow` wins a tie, `*` matches any characters and a trailing `$` anchors the end. An empty `Disallow:` allows everything, and `/robots.txt` is always allowed.
52
+
53
+ ```ts
54
+ import { isAllowedByRobots, parseRobotsTxt } from 'hanzio/sitemap'
55
+
56
+ const rules = parseRobotsTxt(body, 'MyBot/1.0')
57
+ // { allow: ['/cart/help'], disallow: ['/cart/'], sitemaps: [...], crawlDelaySec?: 5 }
58
+
59
+ isAllowedByRobots(rules, 'https://example.com/cart/add?id=1') // false
60
+ isAllowedByRobots(rules, '/cart/help') // true
61
+ ```
@@ -0,0 +1,71 @@
1
+ ---
2
+ description: Typed persisted values over localStorage/sessionStorage or async stores like React Native AsyncStorage, with fallbacks that never throw.
3
+ ---
4
+
5
+ # hanzio/storage
6
+
7
+ `createStorageItem` wraps one key in `localStorage` (or any `SyncStorageLike`) as a typed value. It never throws:
8
+
9
+ - A missing, corrupt (bad JSON) or invalid (`parse` throws) value reads as `fallback`.
10
+ - When the storage throws (quota exceeded, Safari private mode, sandboxed iframes), the latest `set`/`remove` is kept in memory for that item, so reads stay consistent for the session.
11
+ - Without an accessible `globalThis.localStorage` (SSR, Node, Bun, blocked access) it uses a shared in-memory store.
12
+
13
+ ```ts
14
+ import { z } from 'zod'
15
+ import { createStorageItem } from 'hanzio/storage'
16
+
17
+ const PanelPrefs = z.object({ open: z.boolean(), width: z.number() })
18
+
19
+ const panelPrefs = createStorageItem({
20
+ key: 'panel-prefs',
21
+ fallback: { open: false, width: 320 },
22
+ parse: PanelPrefs.parse
23
+ })
24
+
25
+ panelPrefs.get() // { open: false, width: 320 }
26
+ panelPrefs.set({ open: true, width: 400 })
27
+ panelPrefs.remove()
28
+
29
+ // Another tab changed it (the `storage` event). Returns an unsubscribe function.
30
+ const stop = panelPrefs.subscribe((prefs) => console.log(prefs))
31
+ ```
32
+
33
+ Use `sessionStorage` or plain strings:
34
+
35
+ ```ts
36
+ const sendOnEnter = createStorageItem({
37
+ key: 'send-on-enter',
38
+ fallback: false,
39
+ storage: sessionStorage,
40
+ serialize: String,
41
+ deserialize: (raw) => raw === 'true'
42
+ })
43
+ ```
44
+
45
+ ## Async stores
46
+
47
+ `createAsyncStorageItem` takes the same options, but `storage` is required and the methods return promises. It never rejects.
48
+
49
+ ```ts
50
+ import AsyncStorage from '@react-native-async-storage/async-storage'
51
+ import { createAsyncStorageItem } from 'hanzio/storage'
52
+
53
+ const readIds = createAsyncStorageItem({
54
+ key: '@notification_read_session_ids',
55
+ fallback: [] as string[],
56
+ storage: AsyncStorage,
57
+ parse: z.array(z.string()).parse
58
+ })
59
+
60
+ await readIds.set([...(await readIds.get()), 'abc'])
61
+ ```
62
+
63
+ ## API
64
+
65
+ | Export | Description |
66
+ | --- | --- |
67
+ | `createStorageItem(options)` | `{ key, get, set, remove, subscribe }` over a `SyncStorageLike` (default `localStorage`). |
68
+ | `createAsyncStorageItem(options)` | `{ key, get, set, remove }` over an `AsyncStorageLike`. |
69
+ | `createMemoryStorage()` | A `Map`-backed `SyncStorageLike`, for tests and SSR. |
70
+
71
+ Options: `key`, `fallback`, `storage`, `parse` (validate the deserialized value; throwing means `fallback`), `serialize` (default `JSON.stringify`), `deserialize` (default `JSON.parse`).
@@ -0,0 +1,319 @@
1
+ # typedswitch
2
+
3
+ A type-safe, exhaustive switch expression for TypeScript. Handle discriminated unions and string literals with full type inference, compile-time exhaustiveness checking, and async support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install typedswitch
9
+ ```
10
+
11
+ ```bash
12
+ pnpm add typedswitch
13
+ ```
14
+
15
+ ```bash
16
+ bun add typedswitch
17
+ ```
18
+
19
+ ## Features
20
+
21
+ - **Full type inference** — Return types are automatically inferred from your handlers
22
+ - **Exhaustiveness checking** — TypeScript ensures all cases are handled at compile time
23
+ - **Discriminated union support** — Works with any discriminant key, not just `type`
24
+ - **Async-aware** — Async handlers preserve their `Promise` return type
25
+ - **Default handlers** — Handle remaining cases with a fallback
26
+ - **Return type constraints** — Enforce that all handlers return a specific type
27
+ - **`assertNever`** — Exhaustiveness checking for regular `switch` statements
28
+
29
+ ## Usage
30
+
31
+ ### String/Enum Input
32
+
33
+ Switch on a string literal or enum value directly:
34
+
35
+ ```typescript
36
+ import { typedSwitch } from 'typedswitch'
37
+
38
+ type Status = 'success' | 'error' | 'pending'
39
+
40
+ const status: Status = 'success'
41
+
42
+ // All cases required — TypeScript enforces exhaustiveness
43
+ const message = typedSwitch(status, {
44
+ success: () => 'Operation completed!',
45
+ error: () => 'Something went wrong',
46
+ pending: () => 'Please wait...',
47
+ })
48
+ // message: string
49
+ ```
50
+
51
+ Each handler receives the narrowed value:
52
+
53
+ ```typescript
54
+ const result = typedSwitch(status, {
55
+ success: (val) => val.toUpperCase(), // val: 'success'
56
+ error: (val) => val.toUpperCase(), // val: 'error'
57
+ pending: (val) => val.toUpperCase(), // val: 'pending'
58
+ })
59
+ ```
60
+
61
+ ### Discriminated Union Input
62
+
63
+ Switch on objects with a string, number, or boolean discriminant property (like `type`, `kind`, `status`, etc.):
64
+
65
+ ```typescript
66
+ type Event =
67
+ | { type: 'click'; x: number; y: number }
68
+ | { type: 'scroll'; offset: number }
69
+ | { type: 'keypress'; key: string }
70
+
71
+ const event: Event = { type: 'click', x: 100, y: 200 }
72
+
73
+ // Specify the discriminant key as the second argument
74
+ const description = typedSwitch(event, 'type', {
75
+ click: (e) => `Clicked at (${e.x}, ${e.y})`, // e: { type: 'click'; x: number; y: number }
76
+ scroll: (e) => `Scrolled ${e.offset}px`, // e: { type: 'scroll'; offset: number }
77
+ keypress: (e) => `Pressed ${e.key}`, // e: { type: 'keypress'; key: string }
78
+ })
79
+ // description: string
80
+ ```
81
+
82
+ Works with any discriminant key:
83
+
84
+ ```typescript
85
+ type Order =
86
+ | { status: 'pending'; createdAt: Date }
87
+ | { status: 'completed'; completedAt: Date }
88
+ | { status: 'cancelled'; reason: string }
89
+
90
+ const order: Order = { status: 'completed', completedAt: new Date() }
91
+
92
+ const info = typedSwitch(order, 'status', {
93
+ pending: (o) => `Created: ${o.createdAt}`,
94
+ completed: (o) => `Done: ${o.completedAt}`,
95
+ cancelled: (o) => `Cancelled: ${o.reason}`,
96
+ })
97
+ ```
98
+
99
+ Number and boolean discriminants work too. Cases are keyed by `String(value)`:
100
+
101
+ ```typescript
102
+ type Response =
103
+ | { code: 200; body: string }
104
+ | { code: 404; path: string }
105
+
106
+ typedSwitch(response, 'code', {
107
+ 200: (r) => r.body, // r: { code: 200; body: string }
108
+ 404: (r) => `Missing ${r.path}`, // r: { code: 404; path: string }
109
+ })
110
+
111
+ type Result = { ok: true; value: number } | { ok: false; error: string }
112
+
113
+ typedSwitch(result, 'ok', {
114
+ true: (r) => r.value,
115
+ false: (r) => r.error,
116
+ })
117
+ ```
118
+
119
+ ### Default Handler
120
+
121
+ Handle a subset of cases and catch the rest with a default handler:
122
+
123
+ ```typescript
124
+ type Status = 'success' | 'error' | 'pending' | 'cancelled'
125
+
126
+ const status: Status = 'cancelled'
127
+
128
+ // Only handle specific cases, default handles the rest
129
+ const message = typedSwitch(
130
+ status,
131
+ {
132
+ success: () => 'Done!',
133
+ error: () => 'Failed!',
134
+ },
135
+ (val) => `Unhandled status: ${val}` // val: 'pending' | 'cancelled' (unhandled cases only)
136
+ )
137
+ // message: string
138
+ ```
139
+
140
+ Only required, defined handlers narrow the default handler's input. Optional
141
+ handlers (including reusable `PartialStringSwitchCases` and
142
+ `PartialObjectSwitchCases` maps) can be absent, so their cases remain in the
143
+ default handler's input type.
144
+
145
+ Works with discriminated unions too:
146
+
147
+ ```typescript
148
+ const description = typedSwitch(
149
+ event,
150
+ 'type',
151
+ {
152
+ click: (e) => `Clicked at (${e.x}, ${e.y})`,
153
+ },
154
+ (e) => `Unhandled event: ${e.type}` // e: scroll or keypress event (unhandled members only)
155
+ )
156
+ ```
157
+
158
+ ### Async Handlers
159
+
160
+ Mix sync and async handlers freely. `typedSwitch` returns the selected handler's value directly, so async branches preserve their `Promise` return type while sync branches remain sync:
161
+
162
+ ```typescript
163
+ // All sync — returns string
164
+ const syncResult = typedSwitch(status, {
165
+ success: () => 'ok',
166
+ error: () => 'err',
167
+ pending: () => 'wait',
168
+ })
169
+ // syncResult: string
170
+
171
+ // All async — returns Promise<string>
172
+ const asyncResult = typedSwitch(status, {
173
+ success: async () => {
174
+ const data = await fetchData()
175
+ return data.message
176
+ },
177
+ error: async () => 'err',
178
+ pending: async () => 'wait',
179
+ })
180
+ // asyncResult: Promise<string>
181
+
182
+ const message = await asyncResult
183
+
184
+ // Mixed sync/async — returns string | Promise<string>
185
+ const mixedResult = typedSwitch(status, {
186
+ success: () => 'ok',
187
+ error: async () => 'err',
188
+ pending: () => 'wait',
189
+ })
190
+ ```
191
+
192
+ ### Return Type Inference
193
+
194
+ Return types are inferred from all handlers as a union:
195
+
196
+ ```typescript
197
+ const result = typedSwitch(status, {
198
+ success: () => ({ ok: true, data: 'hello' }),
199
+ error: () => ({ ok: false, code: 500 }),
200
+ pending: () => null,
201
+ })
202
+ // result: { ok: true; data: string } | { ok: false; code: number } | null
203
+ ```
204
+
205
+ ### Return Type Constraints
206
+
207
+ Enforce that all handlers return a type extending a constraint with `typedSwitch<Constraint>(...)`. The `<Constraint>` is optional; omit it when you want TypeScript to infer the return types freely.
208
+
209
+ ```typescript
210
+ interface HasId {
211
+ id: string
212
+ }
213
+
214
+ // All handlers must return something with { id: string }
215
+ const result = typedSwitch<HasId>(status, {
216
+ success: () => ({ id: '123', name: 'John' }), // ✓ OK
217
+ error: () => ({ id: '456', code: 500 }), // ✓ OK
218
+ pending: () => ({ id: '789' }), // ✓ OK
219
+ })
220
+ // result: HasId
221
+
222
+ // This would be a compile error:
223
+ typedSwitch<HasId>(status, {
224
+ success: () => ({ name: 'John' }), // ✗ Error: missing 'id'
225
+ error: () => ({ id: '456' }),
226
+ pending: () => ({ id: '789' }),
227
+ })
228
+ ```
229
+
230
+ Constraints also work with discriminated unions and default handlers:
231
+
232
+ ```typescript
233
+ const result = typedSwitch<HasId>(
234
+ event,
235
+ 'type',
236
+ {
237
+ click: () => ({ id: 'click-1', x: 10, y: 20 }),
238
+ scroll: () => ({ id: 'scroll-1', offset: 100 }),
239
+ keypress: () => ({ id: 'key-1', key: 'Enter' }),
240
+ }
241
+ )
242
+ ```
243
+
244
+ ## Error Handling
245
+
246
+ When a case is unhandled at runtime (e.g., due to unsafe casts), `typedSwitch` throws a descriptive error:
247
+
248
+ ```typescript
249
+ const status = 'unknown' as Status
250
+
251
+ typedSwitch(status, {
252
+ success: () => 'ok',
253
+ error: () => 'err',
254
+ })
255
+ // Throws: "Unhandled case: unknown. Available cases: success, error"
256
+ ```
257
+
258
+ For discriminated unions:
259
+
260
+ ```typescript
261
+ // Throws: "Unhandled case: unknown (discriminant key: "type"). Available cases: click, scroll, keypress"
262
+ ```
263
+
264
+ Object discriminants must be strings, numbers, or booleans. Missing or other discriminant values throw a runtime error if they reach `typedSwitch`.
265
+
266
+ ## `assertNever`
267
+
268
+ For regular `switch` statements, put `assertNever` in the `default` branch. It fails to compile if a case is missing, and throws at runtime if an unexpected value gets through:
269
+
270
+ ```typescript
271
+ import { assertNever } from 'typedswitch'
272
+
273
+ function label(status: Status) {
274
+ switch (status) {
275
+ case 'success': return 'Done'
276
+ case 'error': return 'Failed'
277
+ case 'pending': return 'Waiting'
278
+ default: return assertNever(status) // compile error if a case is missing
279
+ }
280
+ }
281
+ ```
282
+
283
+ ## API Reference
284
+
285
+ ### `typedSwitch(value, cases)`
286
+
287
+ Switch on a string literal with exhaustive case handling.
288
+
289
+ ### `typedSwitch(value, cases, defaultHandler)`
290
+
291
+ Switch on a string literal with partial cases and a default fallback.
292
+
293
+ ### `typedSwitch(object, key, cases)`
294
+
295
+ Switch on a discriminated union using the specified discriminant key (string, number, or boolean values).
296
+
297
+ ### `typedSwitch(object, key, cases, defaultHandler)`
298
+
299
+ Switch on a discriminated union with partial cases and a default fallback.
300
+
301
+ ### `typedSwitch<Constraint>(value, cases)`
302
+
303
+ Switch on a string literal while enforcing that each handler returns `Constraint` or `Promise<Constraint>`.
304
+
305
+ ### `typedSwitch<Constraint>(object, key, cases)`
306
+
307
+ Switch on a discriminated union while enforcing that each handler returns `Constraint` or `Promise<Constraint>`.
308
+
309
+ ### `assertNever(value, message?)`
310
+
311
+ Takes a `never` value and always throws (`Unexpected value: ${value}` or `message`). Use it for exhaustive `switch` statements.
312
+
313
+ ### Exported Helper Types
314
+
315
+ `StringSwitchCases`, `PartialStringSwitchCases`, `ObjectSwitchCases`, and `PartialObjectSwitchCases` are exported for users who want to annotate reusable case maps.
316
+
317
+ ## License
318
+
319
+ MIT