hanzio 2.1.0 → 3.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.
@@ -0,0 +1,65 @@
1
+ ---
2
+ description: Typed `{name}` placeholders, interpolation and a small typed translator.
3
+ ---
4
+
5
+ # hanzio/i18n
6
+
7
+ A typed helper for `{name}` message templates, not an i18n framework: no
8
+ plurals, ICU, locale negotiation or escaping.
9
+
10
+ ```ts
11
+ import { createTranslator, extractPlaceholders, interpolate } from 'hanzio/i18n'
12
+
13
+ interpolate('Hi {name}, you have {count} messages', { name: 'Ada', count: 3 })
14
+ // 'Hi Ada, you have 3 messages'
15
+
16
+ interpolate('Hi {name}') // type error: params required
17
+ interpolate('Hi {name}', { nam: 'Ada' }) // type error: misspelled
18
+
19
+ extractPlaceholders('{name} has {count} items, {name}') // ['name', 'count']
20
+ ```
21
+
22
+ ## Placeholders
23
+
24
+ A placeholder is `{` + word characters (`[A-Za-z0-9_]`) + `}`. Anything else is
25
+ plain text: `{ name }`, `{}`, `{count, plural, ...}`. The types and the runtime
26
+ use the same rule.
27
+
28
+ - A placeholder without a param (or with `null`/`undefined`) stays as `{name}`.
29
+ - Only own properties of `params` are used, so `{toString}` is never filled from
30
+ `Object.prototype`.
31
+ - Values are `string | number`, converted with `String()`.
32
+
33
+ Types:
34
+
35
+ | Type | `'Hi {name}, {count}'` |
36
+ | ------------------------ | -------------------------------------------- |
37
+ | `PlaceholderNames<S>` | `'name' \| 'count'` |
38
+ | `PlaceholderParams<S>` | `{ name: string \| number; count: ... }` |
39
+ | `PlaceholderArgs<S>` | `[params: PlaceholderParams<S>]`, `[]` if none |
40
+
41
+ For a non-literal `string` template, names are `string` and params are optional.
42
+
43
+ ## Translator
44
+
45
+ ```ts
46
+ const messages = {
47
+ common: { save: 'Save' },
48
+ greeting: 'Hi {name}'
49
+ } as const
50
+
51
+ const t = createTranslator(messages)
52
+
53
+ t('common.save') // 'Save'
54
+ t('greeting', { name: 'Ada' }) // 'Hi Ada'
55
+ t('greeting') // type error: params required
56
+ t('common.save', { name: 'Ada' }) // type error: no placeholders
57
+ t('common.missing') // type error: unknown key
58
+ ```
59
+
60
+ Keys are the dot paths of the string leaves (`MessageKeys<T>`). Pass the
61
+ messages inline or `as const` so the templates stay literal; with plain
62
+ `Record<string, string>` messages (for example JSON imports) keys are `string`
63
+ and params are optional. A key that is missing at runtime returns the key.
64
+
65
+ `flattenMessages(messages)` gives the flat map: `{ 'common.save': 'Save', greeting: 'Hi {name}' }`.
@@ -0,0 +1,70 @@
1
+ ---
2
+ description: JWT sign/verify/decode using built-in Web Crypto HMAC (HS256/HS384/HS512).
3
+ ---
4
+
5
+ # hanzio/jwt
6
+
7
+ ```ts
8
+ import { jwtDecode, jwtSign, jwtVerify, jwtVerifyResult } from 'hanzio/jwt'
9
+
10
+ const token = await jwtSign({ sub: 'user-1' }, secret, {
11
+ expiresIn: 60 * 60, // seconds
12
+ issuer: 'api',
13
+ audience: 'web'
14
+ })
15
+
16
+ // Payload or null
17
+ const payload = await jwtVerify<{ sub: string }>(token, secret)
18
+
19
+ // Result with a reason
20
+ const result = await jwtVerifyResult(token, secret, {
21
+ issuer: 'api',
22
+ audience: 'web',
23
+ leewaySec: 30
24
+ })
25
+ if (!result.ok) console.log(result.error.code) // 'expired', 'invalid_signature', …
26
+
27
+ // Unverified: for reading claims client-side, never for auth
28
+ const decoded = jwtDecode(token) // { header, payload } | null
29
+ ```
30
+
31
+ ## Signing
32
+
33
+ `jwtSign(payload, secret, options?)`
34
+
35
+ | Option | Claim | Notes |
36
+ | ------------ | ----- | ----------------------------------- |
37
+ | `expiresIn` | `exp` | seconds from now |
38
+ | `notBefore` | `nbf` | seconds from now |
39
+ | `issuer` | `iss` | |
40
+ | `audience` | `aud` | `string \| string[]` |
41
+ | `algorithm` | — | `'HS256'` (default), `'HS384'`, `'HS512'` |
42
+
43
+ - `iat` is always set to the current time.
44
+ - Options win: a claim set through an option replaces the same claim in `payload`.
45
+ Payload claims are kept when the option is omitted.
46
+ - Non-finite `expiresIn`/`notBefore` throw a `TypeError`.
47
+ - The header is `{ alg, typ: 'JWT' }`.
48
+
49
+ ## Verifying
50
+
51
+ `jwtVerifyResult(token, secret, options?)` returns
52
+ `Result<Payload, JwtVerifyError>` (see `hanzio` promise helpers). `error.code` is one of
53
+ `'malformed' | 'unsupported_alg' | 'invalid_signature' | 'expired' | 'not_yet_valid' | 'invalid_issuer' | 'invalid_audience'`.
54
+
55
+ | Option | Default | Notes |
56
+ | ------------ | ----------------------- | ------------------------------------------ |
57
+ | `leewaySec` | `0` | clock skew tolerance for `exp` / `nbf` |
58
+ | `issuer` | — | required `iss` |
59
+ | `audience` | — | must equal `aud` or be one of its entries |
60
+ | `algorithms` | `['HS256','HS384','HS512']` | the header `alg` must be in this list |
61
+
62
+ - The signature is checked before any claim, so unsigned claims never decide the error code.
63
+ - `alg: 'none'` and anything outside `algorithms` is rejected as `unsupported_alg`.
64
+ - Configuration errors (no Web Crypto, empty secret) throw. `jwtVerify` swallows them
65
+ and returns `null`, like any other failure.
66
+
67
+ Imported `CryptoKey`s are cached per secret, algorithm and usage (up to 100 entries).
68
+
69
+ The base64url and UTF-8 helpers this module uses are public in
70
+ [`hanzio/crypto`](../crypto/README.md) (`encodeBase64Url`, `decodeBase64Url`, …).
@@ -0,0 +1,237 @@
1
+ ---
2
+ description: Promise queue with concurrency, priority, timeout, cancellation, and rate-limit controls.
3
+ ---
4
+
5
+ # hanzio/p-queue
6
+
7
+ `hanzio/p-queue` is a small dependency-free promise queue inspired by
8
+ `p-queue`. Use it when you need to run async work with bounded concurrency,
9
+ priority ordering, basic rate limiting, or queue lifecycle hooks.
10
+
11
+ ```ts
12
+ import PQueue from 'hanzio/p-queue'
13
+
14
+ const queue = new PQueue({ concurrency: 2 })
15
+
16
+ const results = await Promise.all([
17
+ queue.add(async () => fetchUser('1')),
18
+ queue.add(async () => fetchUser('2')),
19
+ queue.add(async () => fetchUser('3'))
20
+ ])
21
+
22
+ await queue.onIdle()
23
+ ```
24
+
25
+ ## Common Usage
26
+
27
+ ### Limit Concurrency
28
+
29
+ ```ts
30
+ const queue = new PQueue({ concurrency: 3 })
31
+
32
+ for (const id of userIds) {
33
+ queue.add(async () => syncUser(id))
34
+ }
35
+
36
+ await queue.onIdle()
37
+ ```
38
+
39
+ Only three tasks run at the same time. Queued tasks start as running tasks
40
+ finish.
41
+
42
+ ### Add Multiple Tasks
43
+
44
+ ```ts
45
+ const results = await queue.addAll([
46
+ async () => 'first',
47
+ async () => 'second'
48
+ ])
49
+ ```
50
+
51
+ `addAll` resolves results in the same order as the input functions.
52
+
53
+ ### Pause And Resume
54
+
55
+ ```ts
56
+ const queue = new PQueue({ autoStart: false })
57
+
58
+ queue.add(async () => syncUser('1'))
59
+ queue.add(async () => syncUser('2'))
60
+
61
+ queue.start()
62
+ await queue.onIdle()
63
+ ```
64
+
65
+ Use `pause()` to stop starting new queued tasks. Running tasks continue.
66
+
67
+ ```ts
68
+ queue.pause()
69
+ queue.start()
70
+ ```
71
+
72
+ ### Priority
73
+
74
+ Higher priority tasks run first. Tasks with the same priority keep FIFO order.
75
+
76
+ ```ts
77
+ const queue = new PQueue({ concurrency: 1, autoStart: false })
78
+
79
+ queue.add(async () => 'low', { priority: 0, id: 'low' })
80
+ queue.add(async () => 'high', { priority: 10, id: 'high' })
81
+
82
+ queue.start()
83
+ ```
84
+
85
+ You can update queued task priority by id before it starts:
86
+
87
+ ```ts
88
+ queue.setPriority('low', 20)
89
+ ```
90
+
91
+ ### Timeout
92
+
93
+ Timeouts begin when the task starts running, not while it is waiting in the
94
+ queue.
95
+
96
+ ```ts
97
+ import PQueue, { TimeoutError } from 'hanzio/p-queue'
98
+
99
+ const queue = new PQueue({ timeout: 1000 })
100
+
101
+ try {
102
+ await queue.add(async () => slowTask())
103
+ } catch (error) {
104
+ if (error instanceof TimeoutError) {
105
+ console.log('Task timed out')
106
+ }
107
+ }
108
+ ```
109
+
110
+ Override the timeout per task:
111
+
112
+ ```ts
113
+ await queue.add(async () => slowTask(), { timeout: 5000 })
114
+ ```
115
+
116
+ ### Cancellation
117
+
118
+ Pass an `AbortSignal` to remove a queued task or reject a running task. The task
119
+ also receives the same signal.
120
+
121
+ ```ts
122
+ const controller = new AbortController()
123
+
124
+ const promise = queue.add(
125
+ async ({ signal }) => {
126
+ const response = await fetch(url, { signal })
127
+ return response.json()
128
+ },
129
+ { signal: controller.signal }
130
+ )
131
+
132
+ controller.abort(new Error('Cancelled'))
133
+ await promise
134
+ ```
135
+
136
+ ### Rate Limiting
137
+
138
+ Use `intervalCap` and `interval` to limit how many tasks start during a time
139
+ window.
140
+
141
+ ```ts
142
+ const queue = new PQueue({
143
+ concurrency: 5,
144
+ intervalCap: 10,
145
+ interval: 1000
146
+ })
147
+ ```
148
+
149
+ Enable `strict` to use a sliding window instead of a fixed window.
150
+
151
+ ```ts
152
+ const queue = new PQueue({
153
+ intervalCap: 10,
154
+ interval: 1000,
155
+ strict: true
156
+ })
157
+ ```
158
+
159
+ ## Waiting For Queue State
160
+
161
+ ```ts
162
+ await queue.onEmpty()
163
+ await queue.onIdle()
164
+ await queue.onPendingZero()
165
+ await queue.onSizeLessThan(10)
166
+ await queue.onRateLimit()
167
+ await queue.onRateLimitCleared()
168
+ ```
169
+
170
+ - `onEmpty()` resolves when there are no queued tasks waiting to start.
171
+ - `onIdle()` resolves when there are no queued or running tasks.
172
+ - `onPendingZero()` resolves when all currently running tasks finish.
173
+ - `onSizeLessThan(limit)` resolves when `queue.size < limit`.
174
+ - `onRateLimit()` resolves when the queue becomes rate-limited.
175
+ - `onRateLimitCleared()` resolves when rate limiting clears.
176
+
177
+ ## Events
178
+
179
+ ```ts
180
+ queue.on('active', () => {
181
+ console.log('A task started')
182
+ })
183
+
184
+ queue.on('completed', (result) => {
185
+ console.log('Task completed', result)
186
+ })
187
+
188
+ queue.on('error', (error) => {
189
+ console.error('Task failed', error)
190
+ })
191
+ ```
192
+
193
+ Supported events:
194
+
195
+ - `add`
196
+ - `active`
197
+ - `completed`
198
+ - `error`
199
+ - `empty`
200
+ - `idle`
201
+ - `pendingZero`
202
+ - `next`
203
+ - `rateLimit`
204
+ - `rateLimitCleared`
205
+
206
+ Use `off(event, listener)` to remove a listener.
207
+
208
+ ## State
209
+
210
+ ```ts
211
+ queue.size
212
+ queue.pending
213
+ queue.isPaused
214
+ queue.isRateLimited
215
+ queue.isSaturated
216
+ queue.runningTasks
217
+ queue.concurrency = 5
218
+ queue.timeout = 2000
219
+ ```
220
+
221
+ - `size` is the number of queued tasks waiting to start.
222
+ - `pending` is the number of running tasks.
223
+ - `runningTasks` returns task metadata for currently running tasks.
224
+ - `clear()` removes queued tasks but does not cancel tasks already running.
225
+
226
+ ## Exports
227
+
228
+ ```ts
229
+ import PQueue, {
230
+ PriorityQueue,
231
+ TimeoutError,
232
+ type Options,
233
+ type Queue,
234
+ type QueueAddOptions,
235
+ type TaskOptions
236
+ } from 'hanzio/p-queue'
237
+ ```