hanzio 1.1.0 → 2.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.
- package/README.md +832 -219
- package/dist/api-wrapper/builders.d.ts +15 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +40 -2
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +12 -10
- package/dist/api-wrapper/types.d.ts +104 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +45 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +13 -25
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +8 -13
- package/dist/secrets/loaders.d.ts +5 -12
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1100
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +863 -2809
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +261 -1377
- package/dist/src/secrets/vite.js +6 -17
- package/dist/src/secrets/worker.js +12 -0
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +347 -174
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +37 -26
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +87 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +84 -52
- package/src/api-wrapper/create-client.ts +246 -96
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +117 -2
- package/src/api-wrapper/headers.ts +19 -1
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +43 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +68 -74
- package/src/api-wrapper/types.ts +186 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +235 -83
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +79 -210
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +214 -66
- package/src/secrets/loaders.ts +24 -48
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +134 -103
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/infisical.js +0 -1194
- package/dist/zod/index.js +0 -210
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { ZodError, type z } from 'zod'
|
|
2
|
+
import { anySignal, promiseTimeout } from '../promise'
|
|
3
|
+
import { parseRetryAfter } from '../url'
|
|
2
4
|
import type {
|
|
3
5
|
ApiEndpoint,
|
|
6
|
+
FetchLike,
|
|
4
7
|
OnResponseContext,
|
|
5
8
|
OnRetryContext,
|
|
6
9
|
RetryContext
|
|
7
10
|
} from './types'
|
|
8
|
-
import type { ApiErrorContext, HttpMethod } from './shared'
|
|
11
|
+
import type { ApiErrorContext, HttpMethod, RequestMeta } from './shared'
|
|
9
12
|
import {
|
|
13
|
+
ApiResponseError,
|
|
10
14
|
ConfigError,
|
|
11
15
|
HttpResponseError,
|
|
16
|
+
isNonRetryableApiError,
|
|
12
17
|
NetworkError,
|
|
13
18
|
RequestAbortedError,
|
|
14
19
|
RequestTimeoutError,
|
|
15
|
-
RequestValidationError,
|
|
16
20
|
ResponseValidationError
|
|
17
21
|
} from './errors'
|
|
18
22
|
import {
|
|
@@ -20,17 +24,16 @@ import {
|
|
|
20
24
|
getHeadersAsObject,
|
|
21
25
|
hasJsonContentType
|
|
22
26
|
} from './headers'
|
|
27
|
+
import { redactUrl } from './redaction'
|
|
23
28
|
import { makeJsonParseZodError } from './zod-issues'
|
|
24
29
|
|
|
25
30
|
export type RequestConfig = {
|
|
26
31
|
method: HttpMethod
|
|
27
|
-
|
|
28
|
-
queryParams: Record<string, string>
|
|
32
|
+
fullUrl: string
|
|
29
33
|
body: unknown
|
|
30
34
|
headers: Record<string, string>
|
|
31
|
-
doNotEncodeQueryParams?: boolean
|
|
32
35
|
timeoutMs?: number
|
|
33
|
-
fetchFn:
|
|
36
|
+
fetchFn: FetchLike
|
|
34
37
|
userSignal?: AbortSignal
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -53,26 +56,23 @@ export async function httpRequest(
|
|
|
53
56
|
config: RequestConfig,
|
|
54
57
|
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
|
|
55
58
|
): Promise<Response> {
|
|
56
|
-
const queryString = config.doNotEncodeQueryParams
|
|
57
|
-
? Object.entries(config.queryParams)
|
|
58
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
59
|
-
.join('&')
|
|
60
|
-
: new URLSearchParams(config.queryParams).toString()
|
|
61
|
-
const fullUrl = queryString ? `${config.url}?${queryString}` : config.url
|
|
62
|
-
|
|
63
59
|
const timeoutController = new AbortController()
|
|
64
60
|
const timeout =
|
|
65
61
|
config.timeoutMs === undefined
|
|
66
62
|
? undefined
|
|
67
63
|
: setTimeout(() => timeoutController.abort(), config.timeoutMs)
|
|
68
64
|
|
|
69
|
-
const signal =
|
|
65
|
+
const signal = anySignal(timeoutController.signal, config.userSignal)
|
|
70
66
|
|
|
71
67
|
try {
|
|
72
|
-
return await config.fetchFn(fullUrl, {
|
|
68
|
+
return await config.fetchFn(config.fullUrl, {
|
|
73
69
|
method: config.method,
|
|
74
70
|
headers: config.headers,
|
|
75
|
-
body: buildFetchBody(
|
|
71
|
+
body: buildFetchBody(
|
|
72
|
+
config.body,
|
|
73
|
+
config.headers,
|
|
74
|
+
(msg) => new ConfigError(msg, ctx())
|
|
75
|
+
),
|
|
76
76
|
signal
|
|
77
77
|
})
|
|
78
78
|
} catch (error) {
|
|
@@ -85,6 +85,7 @@ export async function httpRequest(
|
|
|
85
85
|
}
|
|
86
86
|
throw new RequestAbortedError(ctx(), error)
|
|
87
87
|
}
|
|
88
|
+
if (error instanceof ConfigError) throw error
|
|
88
89
|
throw new NetworkError(
|
|
89
90
|
error instanceof Error ? error.message : String(error),
|
|
90
91
|
ctx(),
|
|
@@ -104,27 +105,6 @@ function isDomAbortError(error: unknown): boolean {
|
|
|
104
105
|
)
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
export function mergeSignals(
|
|
108
|
-
a: AbortSignal,
|
|
109
|
-
b: AbortSignal | undefined
|
|
110
|
-
): AbortSignal {
|
|
111
|
-
if (!b) return a
|
|
112
|
-
const anyFn = (
|
|
113
|
-
AbortSignal as unknown as {
|
|
114
|
-
any?: (signals: AbortSignal[]) => AbortSignal
|
|
115
|
-
}
|
|
116
|
-
).any
|
|
117
|
-
if (typeof anyFn === 'function') return anyFn([a, b])
|
|
118
|
-
|
|
119
|
-
const controller = new AbortController()
|
|
120
|
-
const onAbort = () => controller.abort()
|
|
121
|
-
if (a.aborted) controller.abort()
|
|
122
|
-
else a.addEventListener('abort', onAbort, { once: true })
|
|
123
|
-
if (b.aborted) controller.abort()
|
|
124
|
-
else b.addEventListener('abort', onAbort, { once: true })
|
|
125
|
-
return controller.signal
|
|
126
|
-
}
|
|
127
|
-
|
|
128
108
|
export async function readErrorResponse(response: Response): Promise<{
|
|
129
109
|
body: string
|
|
130
110
|
headers: Record<string, string>
|
|
@@ -142,30 +122,21 @@ export function getRetryDelay(params: {
|
|
|
142
122
|
retryDelay: number | ((attempt: number) => number)
|
|
143
123
|
attempt: number
|
|
144
124
|
response?: Response
|
|
125
|
+
maxRetryDelayMs?: number
|
|
145
126
|
}): number {
|
|
127
|
+
const maxDelay = params.maxRetryDelayMs ?? 30_000
|
|
146
128
|
const { retryDelay, attempt, response } = params
|
|
147
129
|
if (response && (response.status === 429 || response.status === 503)) {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
const seconds = Number(retryAfter)
|
|
151
|
-
if (Number.isFinite(seconds)) return seconds * 1000
|
|
152
|
-
const dateMs = Date.parse(retryAfter)
|
|
153
|
-
if (!Number.isNaN(dateMs)) {
|
|
154
|
-
return Math.max(0, dateMs - Date.now())
|
|
155
|
-
}
|
|
156
|
-
}
|
|
130
|
+
const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'))
|
|
131
|
+
if (retryAfterMs !== undefined) return Math.min(retryAfterMs, maxDelay)
|
|
157
132
|
}
|
|
158
|
-
|
|
159
|
-
|
|
133
|
+
const delayMs =
|
|
134
|
+
typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay
|
|
135
|
+
return Math.min(delayMs, maxDelay)
|
|
160
136
|
}
|
|
161
137
|
|
|
162
138
|
export function shouldAttemptRetryOnError(error: unknown): boolean {
|
|
163
|
-
|
|
164
|
-
if (error instanceof ResponseValidationError) return false
|
|
165
|
-
if (error instanceof RequestValidationError) return false
|
|
166
|
-
if (error instanceof ConfigError) return false
|
|
167
|
-
if (error instanceof RequestAbortedError) return false
|
|
168
|
-
return true
|
|
139
|
+
return !isNonRetryableApiError(error)
|
|
169
140
|
}
|
|
170
141
|
|
|
171
142
|
export function shouldAttemptRetryOnHttp(retryCtx: RetryContext): boolean {
|
|
@@ -178,14 +149,15 @@ type MakeRequestArgs<TValidated> = {
|
|
|
178
149
|
config: RequestConfig
|
|
179
150
|
maxRetries: number
|
|
180
151
|
retryDelay: number | ((attempt: number) => number)
|
|
181
|
-
|
|
152
|
+
maxRetryDelayMs?: number
|
|
153
|
+
validateFn: (response: Response, attempt: number) => Promise<TValidated>
|
|
182
154
|
shouldRetry: (context: RetryContext) => boolean
|
|
183
155
|
logger?: Pick<Console, 'debug'>
|
|
184
156
|
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
|
|
185
157
|
endpointName: string
|
|
186
158
|
method: HttpMethod
|
|
187
159
|
fullUrl: string
|
|
188
|
-
meta?:
|
|
160
|
+
meta?: RequestMeta
|
|
189
161
|
onResponse?: (context: OnResponseContext) => void | Promise<void>
|
|
190
162
|
onRetry?: (context: OnRetryContext) => void | Promise<void>
|
|
191
163
|
}
|
|
@@ -201,6 +173,7 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
201
173
|
config,
|
|
202
174
|
maxRetries,
|
|
203
175
|
retryDelay,
|
|
176
|
+
maxRetryDelayMs,
|
|
204
177
|
validateFn,
|
|
205
178
|
shouldRetry,
|
|
206
179
|
logger,
|
|
@@ -217,7 +190,13 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
217
190
|
while (true) {
|
|
218
191
|
let response: Response | undefined
|
|
219
192
|
try {
|
|
220
|
-
|
|
193
|
+
// Headers and body stay out of debug logs; they may carry credentials.
|
|
194
|
+
logger?.debug?.('API request', {
|
|
195
|
+
endpoint: endpointName,
|
|
196
|
+
method,
|
|
197
|
+
url: redactUrl(fullUrl),
|
|
198
|
+
attempt: retryCount
|
|
199
|
+
})
|
|
221
200
|
response = await httpRequest(config, (over) =>
|
|
222
201
|
ctx({ attempt: retryCount, ...over })
|
|
223
202
|
)
|
|
@@ -244,15 +223,18 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
244
223
|
const delayMs = getRetryDelay({
|
|
245
224
|
retryDelay,
|
|
246
225
|
attempt: retryCount,
|
|
247
|
-
response
|
|
226
|
+
response,
|
|
227
|
+
maxRetryDelayMs
|
|
248
228
|
})
|
|
249
229
|
await onRetry?.({
|
|
250
230
|
...retryCtx,
|
|
251
231
|
delayMs,
|
|
252
232
|
nextAttempt: retryCount + 1
|
|
253
233
|
})
|
|
234
|
+
await sleepBeforeRetry(delayMs, config.userSignal, () =>
|
|
235
|
+
ctx({ attempt: retryCount })
|
|
236
|
+
)
|
|
254
237
|
retryCount++
|
|
255
|
-
await delay(delayMs)
|
|
256
238
|
continue
|
|
257
239
|
}
|
|
258
240
|
|
|
@@ -267,17 +249,15 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
267
249
|
}
|
|
268
250
|
|
|
269
251
|
return {
|
|
270
|
-
validatedData: await validateFn(response),
|
|
252
|
+
validatedData: await validateFn(response, retryCount),
|
|
271
253
|
retryCount,
|
|
272
254
|
httpStatus: response.status
|
|
273
255
|
}
|
|
274
256
|
} catch (error) {
|
|
257
|
+
// ApiResponseError is not retried by default, but a custom shouldRetry may opt in.
|
|
275
258
|
if (
|
|
276
|
-
error
|
|
277
|
-
error instanceof
|
|
278
|
-
error instanceof RequestValidationError ||
|
|
279
|
-
error instanceof ConfigError ||
|
|
280
|
-
error instanceof RequestAbortedError
|
|
259
|
+
isNonRetryableApiError(error) &&
|
|
260
|
+
!(error instanceof ApiResponseError)
|
|
281
261
|
) {
|
|
282
262
|
throw error
|
|
283
263
|
}
|
|
@@ -297,15 +277,18 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
297
277
|
const delayMs = getRetryDelay({
|
|
298
278
|
retryDelay,
|
|
299
279
|
attempt: retryCount,
|
|
300
|
-
response: undefined
|
|
280
|
+
response: undefined,
|
|
281
|
+
maxRetryDelayMs
|
|
301
282
|
})
|
|
302
283
|
await onRetry?.({
|
|
303
284
|
...retryCtx,
|
|
304
285
|
delayMs,
|
|
305
286
|
nextAttempt: retryCount + 1
|
|
306
287
|
})
|
|
288
|
+
await sleepBeforeRetry(delayMs, config.userSignal, () =>
|
|
289
|
+
ctx({ attempt: retryCount })
|
|
290
|
+
)
|
|
307
291
|
retryCount++
|
|
308
|
-
await delay(delayMs)
|
|
309
292
|
}
|
|
310
293
|
}
|
|
311
294
|
}
|
|
@@ -320,28 +303,31 @@ export async function validateAndTransformResponse<T extends z.ZodType>(
|
|
|
320
303
|
>,
|
|
321
304
|
response: Response,
|
|
322
305
|
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
|
|
323
|
-
): Promise<z.infer<T
|
|
306
|
+
): Promise<{ data: z.infer<T>; responseSizeBytes: number }> {
|
|
324
307
|
const responseHeaders = getHeadersAsObject(response.headers)
|
|
308
|
+
const text = await response.text()
|
|
309
|
+
const responseSizeBytes = new TextEncoder().encode(text).length
|
|
325
310
|
let responseData: unknown
|
|
326
311
|
if (hasJsonContentType(responseHeaders)) {
|
|
327
|
-
const text = await response.text()
|
|
328
312
|
try {
|
|
329
313
|
responseData = text === '' ? undefined : JSON.parse(text)
|
|
330
314
|
} catch (error) {
|
|
331
315
|
throw new ResponseValidationError(
|
|
332
316
|
makeJsonParseZodError(error, text),
|
|
333
317
|
text,
|
|
334
|
-
ctx()
|
|
318
|
+
ctx(),
|
|
319
|
+
{ invalidJson: true }
|
|
335
320
|
)
|
|
336
321
|
}
|
|
337
322
|
} else {
|
|
338
|
-
responseData =
|
|
323
|
+
responseData = text
|
|
339
324
|
}
|
|
340
325
|
const transformedData = endpoint.resFormatter
|
|
341
326
|
? endpoint.resFormatter(responseData, responseHeaders)
|
|
342
327
|
: responseData
|
|
343
328
|
|
|
344
|
-
|
|
329
|
+
const data = await validateResponse(endpoint.resSchema, transformedData, ctx)
|
|
330
|
+
return { data, responseSizeBytes }
|
|
345
331
|
}
|
|
346
332
|
|
|
347
333
|
export function defaultShouldRetry(ctx: RetryContext): boolean {
|
|
@@ -349,6 +335,14 @@ export function defaultShouldRetry(ctx: RetryContext): boolean {
|
|
|
349
335
|
return shouldAttemptRetryOnHttp(ctx)
|
|
350
336
|
}
|
|
351
337
|
|
|
352
|
-
function
|
|
353
|
-
|
|
338
|
+
async function sleepBeforeRetry(
|
|
339
|
+
ms: number,
|
|
340
|
+
signal: AbortSignal | undefined,
|
|
341
|
+
ctx: () => ApiErrorContext
|
|
342
|
+
): Promise<void> {
|
|
343
|
+
try {
|
|
344
|
+
await promiseTimeout(ms, { signal })
|
|
345
|
+
} catch (error) {
|
|
346
|
+
throw new RequestAbortedError(ctx(), error)
|
|
347
|
+
}
|
|
354
348
|
}
|
package/src/api-wrapper/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
|
+
import type { Result } from '../promise'
|
|
2
3
|
import type { ApiError } from './errors'
|
|
3
4
|
import type {
|
|
4
5
|
ApiErrorContext,
|
|
@@ -6,7 +7,8 @@ import type {
|
|
|
6
7
|
HttpMethod,
|
|
7
8
|
PathParams,
|
|
8
9
|
QueryParams,
|
|
9
|
-
RequestBodyFormat
|
|
10
|
+
RequestBodyFormat,
|
|
11
|
+
RequestMeta
|
|
10
12
|
} from './shared'
|
|
11
13
|
|
|
12
14
|
export interface ApiEndpoint<
|
|
@@ -26,10 +28,11 @@ export interface ApiEndpoint<
|
|
|
26
28
|
baseApiUrl?: string
|
|
27
29
|
reqBodyFormat?: RequestBodyFormat
|
|
28
30
|
defaultHeaders?: Record<string, string>
|
|
31
|
+
/** Reshapes the raw body before `resSchema` parses it, so it returns the schema's input type. */
|
|
29
32
|
resFormatter?: (
|
|
30
33
|
data: unknown,
|
|
31
34
|
headers: Record<string, string>
|
|
32
|
-
) => z.
|
|
35
|
+
) => z.input<TResponse>
|
|
33
36
|
reqDefaultQueryParams?: QueryParams
|
|
34
37
|
doNotEncodeQueryParams?: boolean
|
|
35
38
|
}
|
|
@@ -72,7 +75,7 @@ export type OnRequestContext = {
|
|
|
72
75
|
url: string
|
|
73
76
|
headers: Record<string, string>
|
|
74
77
|
body: unknown
|
|
75
|
-
meta?:
|
|
78
|
+
meta?: RequestMeta
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export type OnResponseContext = {
|
|
@@ -81,7 +84,7 @@ export type OnResponseContext = {
|
|
|
81
84
|
url: string
|
|
82
85
|
response: Response
|
|
83
86
|
attempt: number
|
|
84
|
-
meta?:
|
|
87
|
+
meta?: RequestMeta
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
export type ActionCache = {
|
|
@@ -98,7 +101,7 @@ export type ActionCache = {
|
|
|
98
101
|
|
|
99
102
|
export type ActionInvokeOptions = {
|
|
100
103
|
signal?: AbortSignal
|
|
101
|
-
meta?:
|
|
104
|
+
meta?: RequestMeta
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
// biome-ignore lint/suspicious/noExplicitAny: TApi defaults to any so plain ApiAction stays open
|
|
@@ -106,7 +109,7 @@ export type ActionContext<TInput, TApi = any> = {
|
|
|
106
109
|
input: TInput
|
|
107
110
|
api: TApi
|
|
108
111
|
signal?: AbortSignal
|
|
109
|
-
meta?:
|
|
112
|
+
meta?: RequestMeta
|
|
110
113
|
logger?: Pick<Console, 'debug' | 'error'>
|
|
111
114
|
cache: ActionCache
|
|
112
115
|
}
|
|
@@ -114,6 +117,8 @@ export type ActionContext<TInput, TApi = any> = {
|
|
|
114
117
|
// biome-ignore lint/suspicious/noExplicitAny: variance-friendly bounds
|
|
115
118
|
export interface ApiAction<TInput = any, TOutput = any> {
|
|
116
119
|
handler: (ctx: ActionContext<TInput>) => Promise<TOutput> | TOutput
|
|
120
|
+
/** Validates the caller's input at runtime; `handler` receives the parsed output. */
|
|
121
|
+
readonly input?: z.ZodType
|
|
117
122
|
/** When true, the action accepts only optional `{ signal, meta }`; input is always `undefined`. */
|
|
118
123
|
readonly noRuntimeInput?: boolean
|
|
119
124
|
}
|
|
@@ -122,7 +127,18 @@ type ActionDefinition<TInput, TOutput, TApi> = {
|
|
|
122
127
|
handler: (ctx: ActionContext<TInput, TApi>) => Promise<TOutput> | TOutput
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
type SchemaActionDefinition<TSchema extends z.ZodType, TOutput, TApi> = {
|
|
131
|
+
input: TSchema
|
|
132
|
+
handler: (
|
|
133
|
+
ctx: ActionContext<z.output<TSchema>, TApi>
|
|
134
|
+
) => Promise<TOutput> | TOutput
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
export type DefineAction<TApi> = {
|
|
138
|
+
/** Input validated by a Zod schema: callers pass `z.input`, the handler gets `z.output`. */
|
|
139
|
+
<TSchema extends z.ZodType, TOutput>(
|
|
140
|
+
def: SchemaActionDefinition<TSchema, TOutput, TApi>
|
|
141
|
+
): ApiAction<z.input<TSchema>, TOutput>
|
|
126
142
|
<TOutput>(
|
|
127
143
|
def: ActionDefinition<undefined, TOutput, TApi>
|
|
128
144
|
): ApiAction<undefined, TOutput>
|
|
@@ -140,24 +156,68 @@ export type ActionArgs<A> = [ActionInput<A>] extends [undefined]
|
|
|
140
156
|
? [options?: ActionInvokeOptions]
|
|
141
157
|
: [input: ActionInput<A>, options?: ActionInvokeOptions]
|
|
142
158
|
|
|
159
|
+
// Tail-recursive (accumulator) to stay under TypeScript's recursion limit.
|
|
160
|
+
type CharsOf<
|
|
161
|
+
S extends string,
|
|
162
|
+
Acc extends string = never
|
|
163
|
+
> = S extends `${infer C}${infer Rest}` ? CharsOf<Rest, Acc | C> : Acc
|
|
164
|
+
type IdentStart =
|
|
165
|
+
CharsOf<'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'>
|
|
166
|
+
type WordChar = IdentStart | CharsOf<'0123456789'>
|
|
167
|
+
type TakeWord<
|
|
168
|
+
S extends string,
|
|
169
|
+
Acc extends string = ''
|
|
170
|
+
> = S extends `${infer C}${infer Rest}`
|
|
171
|
+
? C extends WordChar
|
|
172
|
+
? TakeWord<Rest, `${Acc}${C}`>
|
|
173
|
+
: Acc
|
|
174
|
+
: Acc
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `:name` placeholders in a literal path, matching the runtime rule (a letter
|
|
178
|
+
* or `_`, then word characters, so ports like `:8080` don't count).
|
|
179
|
+
* `never` for non-literal paths.
|
|
180
|
+
*/
|
|
181
|
+
export type PathParamNames<TPath> = TPath extends string
|
|
182
|
+
? string extends TPath
|
|
183
|
+
? never
|
|
184
|
+
: TPath extends `${string}:${infer Rest}`
|
|
185
|
+
?
|
|
186
|
+
| (Rest extends `${IdentStart}${string}` ? TakeWord<Rest> : never)
|
|
187
|
+
| PathParamNames<Rest>
|
|
188
|
+
: never
|
|
189
|
+
: never
|
|
190
|
+
|
|
191
|
+
type EndpointPathParamNames<TEndpoint extends ApiEndpoint> = PathParamNames<
|
|
192
|
+
TEndpoint['path']
|
|
193
|
+
>
|
|
194
|
+
|
|
143
195
|
type SchemaInputKeys<TEndpoint extends ApiEndpoint> =
|
|
144
196
|
| (TEndpoint['reqBodySchema'] extends z.ZodType ? 'reqBody' : never)
|
|
145
|
-
| (TEndpoint['reqParamsSchema'] extends z.ZodType ? 'reqParams' : never)
|
|
146
197
|
| (TEndpoint['reqQuerySchema'] extends z.ZodType ? 'reqQuery' : never)
|
|
147
198
|
| (TEndpoint['reqHeadersSchema'] extends z.ZodType ? 'reqHeaders' : never)
|
|
199
|
+
| (TEndpoint['reqParamsSchema'] extends z.ZodType
|
|
200
|
+
? 'reqParams'
|
|
201
|
+
: [EndpointPathParamNames<TEndpoint>] extends [never]
|
|
202
|
+
? never
|
|
203
|
+
: 'reqParams')
|
|
148
204
|
|
|
205
|
+
// Callers pass what the schemas accept (`z.input`): fields with `.default()`
|
|
206
|
+
// are optional and transforms take their pre-transform type.
|
|
149
207
|
type SchemaInputs<TEndpoint extends ApiEndpoint> = {
|
|
150
208
|
reqBody: TEndpoint['reqBodySchema'] extends z.ZodType
|
|
151
|
-
? z.
|
|
209
|
+
? z.input<TEndpoint['reqBodySchema']>
|
|
152
210
|
: unknown
|
|
153
211
|
reqParams: TEndpoint['reqParamsSchema'] extends z.ZodType
|
|
154
|
-
? z.
|
|
155
|
-
:
|
|
212
|
+
? z.input<TEndpoint['reqParamsSchema']>
|
|
213
|
+
: [EndpointPathParamNames<TEndpoint>] extends [never]
|
|
214
|
+
? PathParams
|
|
215
|
+
: Record<EndpointPathParamNames<TEndpoint>, string | number>
|
|
156
216
|
reqQuery: TEndpoint['reqQuerySchema'] extends z.ZodType
|
|
157
|
-
? z.
|
|
217
|
+
? z.input<TEndpoint['reqQuerySchema']>
|
|
158
218
|
: QueryParams
|
|
159
219
|
reqHeaders: TEndpoint['reqHeadersSchema'] extends z.ZodType
|
|
160
|
-
? z.
|
|
220
|
+
? z.input<TEndpoint['reqHeadersSchema']>
|
|
161
221
|
: Record<string, string>
|
|
162
222
|
}
|
|
163
223
|
|
|
@@ -166,8 +226,8 @@ type CommonRequestOptions = {
|
|
|
166
226
|
signal?: AbortSignal
|
|
167
227
|
timeoutMs?: number
|
|
168
228
|
retries?: number
|
|
169
|
-
fetch?:
|
|
170
|
-
meta?:
|
|
229
|
+
fetch?: FetchLike
|
|
230
|
+
meta?: RequestMeta
|
|
171
231
|
}
|
|
172
232
|
|
|
173
233
|
export type RequestInput<TEndpoint extends ApiEndpoint> = Pick<
|
|
@@ -190,21 +250,72 @@ export type ApiClient<
|
|
|
190
250
|
<K extends keyof T>(
|
|
191
251
|
endpointKey: K,
|
|
192
252
|
...args: RequestArgs<T[K]>
|
|
193
|
-
): Promise<ApiWrapperResponse<
|
|
253
|
+
): Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>
|
|
194
254
|
<K extends keyof A>(
|
|
195
255
|
actionKey: K,
|
|
196
256
|
...args: ActionArgs<A[K]>
|
|
197
257
|
): Promise<ActionOutput<A[K]>>
|
|
198
258
|
}
|
|
199
259
|
cache: ActionCache
|
|
260
|
+
/**
|
|
261
|
+
* The same endpoints and actions, resolving to `Result` instead of throwing
|
|
262
|
+
* `ApiError`s. Other errors (bugs in hooks or middleware) still throw.
|
|
263
|
+
*/
|
|
264
|
+
safe: {
|
|
265
|
+
[K in keyof T]: (
|
|
266
|
+
...args: RequestArgs<T[K]>
|
|
267
|
+
) => Promise<Result<ApiWrapperResponse<EndpointResponse<T[K]>>, ApiError>>
|
|
268
|
+
} & {
|
|
269
|
+
[K in keyof A]: (
|
|
270
|
+
...args: ActionArgs<A[K]>
|
|
271
|
+
) => Promise<Result<ActionOutput<A[K]>, ApiError>>
|
|
272
|
+
}
|
|
200
273
|
} & {
|
|
201
274
|
[K in keyof T]: (
|
|
202
275
|
...args: RequestArgs<T[K]>
|
|
203
|
-
) => Promise<ApiWrapperResponse<
|
|
276
|
+
) => Promise<ApiWrapperResponse<EndpointResponse<T[K]>>>
|
|
204
277
|
} & {
|
|
205
278
|
[K in keyof A]: (...args: ActionArgs<A[K]>) => Promise<ActionOutput<A[K]>>
|
|
206
279
|
}
|
|
207
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Any client, for helpers that accept whichever client they're given. Call
|
|
283
|
+
* endpoints through `request` or `safe`; results are `unknown`.
|
|
284
|
+
*/
|
|
285
|
+
export type AnyApiClient = {
|
|
286
|
+
// Method syntax keeps the key parameter bivariant, so any client fits.
|
|
287
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts every client's argument shapes
|
|
288
|
+
request(key: string, ...args: any[]): Promise<unknown>
|
|
289
|
+
cache: ActionCache
|
|
290
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts every client's argument shapes
|
|
291
|
+
safe: Record<string, (...args: any[]) => Promise<Result<unknown, ApiError>>>
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Parsed response data of an endpoint definition (`z.output` of `resSchema`). */
|
|
295
|
+
export type EndpointResponse<TEndpoint extends ApiEndpoint> = z.output<
|
|
296
|
+
TEndpoint['resSchema']
|
|
297
|
+
>
|
|
298
|
+
|
|
299
|
+
/** Request input for an endpoint definition (`reqBody`, `reqParams`, …). */
|
|
300
|
+
export type EndpointRequest<TEndpoint extends ApiEndpoint> =
|
|
301
|
+
RequestInput<TEndpoint>
|
|
302
|
+
|
|
303
|
+
/** Response data of a client method: `InferResponse<typeof api, 'usersGet'>`. */
|
|
304
|
+
export type InferResponse<
|
|
305
|
+
TClient,
|
|
306
|
+
K extends keyof TClient
|
|
307
|
+
> = TClient[K] extends (...args: never[]) => Promise<infer R>
|
|
308
|
+
? R extends ApiWrapperResponse<infer D>
|
|
309
|
+
? D
|
|
310
|
+
: R
|
|
311
|
+
: never
|
|
312
|
+
|
|
313
|
+
/** First argument of a client method: `InferRequest<typeof api, 'usersGet'>`. */
|
|
314
|
+
export type InferRequest<
|
|
315
|
+
TClient,
|
|
316
|
+
K extends keyof TClient
|
|
317
|
+
> = TClient[K] extends (...args: infer P) => unknown ? NonNullable<P[0]> : never
|
|
318
|
+
|
|
208
319
|
export type ActionsFactoryHelpers<
|
|
209
320
|
T extends Record<string, ApiEndpoint>,
|
|
210
321
|
A extends Record<string, ApiAction> = Record<string, never>
|
|
@@ -213,6 +324,48 @@ export type ActionsFactoryHelpers<
|
|
|
213
324
|
defineAction: DefineAction<ApiClient<T, A>>
|
|
214
325
|
}
|
|
215
326
|
|
|
327
|
+
/** Any fetch-compatible function; avoids Bun's `typeof fetch` extras like `preconnect`. */
|
|
328
|
+
export type FetchLike = (
|
|
329
|
+
input: string | URL | Request,
|
|
330
|
+
init?: RequestInit
|
|
331
|
+
) => Promise<Response>
|
|
332
|
+
|
|
333
|
+
export type ApiMiddlewareContext = {
|
|
334
|
+
/** The client's `name`, when set. */
|
|
335
|
+
client?: string
|
|
336
|
+
/** The endpoint key, e.g. `usersGet`. */
|
|
337
|
+
endpoint: string
|
|
338
|
+
method: HttpMethod
|
|
339
|
+
/** Full URL including the query string. */
|
|
340
|
+
url: string
|
|
341
|
+
/** Outgoing headers. Mutate before `next()` to add headers (e.g. `traceparent`). */
|
|
342
|
+
headers: Record<string, string>
|
|
343
|
+
meta?: RequestMeta
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Wraps one logical request: `onRequest`, every retry attempt, validation and
|
|
348
|
+
* `checkResponse`. Call `next()` to continue; errors it throws are `ApiError`s
|
|
349
|
+
* that were already redacted and passed to `onError`.
|
|
350
|
+
*/
|
|
351
|
+
export type ApiMiddleware = (
|
|
352
|
+
context: ApiMiddlewareContext,
|
|
353
|
+
next: () => Promise<ApiWrapperResponse<unknown>>
|
|
354
|
+
) => Promise<ApiWrapperResponse<unknown>>
|
|
355
|
+
|
|
356
|
+
export type CheckResponseContext = {
|
|
357
|
+
endpoint: string
|
|
358
|
+
method: HttpMethod
|
|
359
|
+
url: string
|
|
360
|
+
httpStatus: number
|
|
361
|
+
attempt: number
|
|
362
|
+
meta?: RequestMeta
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export type DefaultHeaders =
|
|
366
|
+
| Record<string, string>
|
|
367
|
+
| (() => Record<string, string> | Promise<Record<string, string>>)
|
|
368
|
+
|
|
216
369
|
export interface ApiClientConfig<
|
|
217
370
|
T extends Record<string, ApiEndpoint>,
|
|
218
371
|
A extends Record<string, ApiAction> = Record<string, never>
|
|
@@ -221,13 +374,26 @@ export interface ApiClientConfig<
|
|
|
221
374
|
baseApiUrls: Record<string, BaseApiUrl>
|
|
222
375
|
defaultBaseApiUrl?: string
|
|
223
376
|
endpoints: T
|
|
224
|
-
|
|
377
|
+
/** Static headers, or a function resolved on every request (e.g. auth from a secret store). */
|
|
378
|
+
defaultHeaders?: DefaultHeaders
|
|
225
379
|
timeoutMs?: number
|
|
226
380
|
retries?: number
|
|
227
381
|
retryDelayMs?: number | ((attempt: number) => number)
|
|
382
|
+
maxRetryDelayMs?: number
|
|
228
383
|
shouldRetry?: (context: RetryContext) => boolean
|
|
229
384
|
logger?: Pick<Console, 'debug' | 'error'>
|
|
230
|
-
fetch?:
|
|
385
|
+
fetch?: FetchLike
|
|
386
|
+
/** Middleware around each whole request, in array order (first is outermost). */
|
|
387
|
+
use?: ApiMiddleware[]
|
|
388
|
+
/**
|
|
389
|
+
* Runs after `resSchema` validation on every attempt. Throw to reject the
|
|
390
|
+
* response; non-`ApiError` throws become `ApiResponseError`, which the
|
|
391
|
+
* default retry policy does not retry.
|
|
392
|
+
*/
|
|
393
|
+
checkResponse?: (
|
|
394
|
+
data: unknown,
|
|
395
|
+
context: CheckResponseContext
|
|
396
|
+
) => void | Promise<void>
|
|
231
397
|
onRequest?: (
|
|
232
398
|
context: OnRequestContext
|
|
233
399
|
) => RequestInterceptorResult | Promise<RequestInterceptorResult>
|
|
@@ -249,6 +415,8 @@ export type ApiClientConfigWithActions<
|
|
|
249
415
|
}
|
|
250
416
|
|
|
251
417
|
export type {
|
|
418
|
+
ApiRequestMeta,
|
|
419
|
+
RequestMeta,
|
|
252
420
|
BaseApiUrl,
|
|
253
421
|
HttpMethod,
|
|
254
422
|
PathParams,
|