hanzio 1.1.0 → 1.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.
- package/README.md +70 -4
- package/dist/api-wrapper/builders.d.ts +13 -2
- package/dist/api-wrapper/errors.d.ts +1 -0
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/transport.d.ts +8 -5
- package/dist/api-wrapper/types.d.ts +1 -0
- package/dist/cache/index.d.ts +26 -0
- 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/src/api-wrapper/index.js +152 -122
- package/dist/src/index.js +3173 -2713
- package/dist/src/jwt/index.js +2 -2
- package/dist/src/p-queue/index.js +3 -3
- package/dist/src/secrets/index.js +670 -402
- package/dist/src/secrets/infisical.js +451 -168
- package/dist/src/secrets/loaders.js +467 -174
- package/dist/src/secrets/vite.js +33 -11
- package/dist/src/secrets/worker.js +275 -0
- package/dist/src/sitemap/index.js +1 -1
- package/dist/src/zod/index.js +7 -7
- package/dist/typedswitch/index.d.ts +9 -5
- package/package.json +12 -3
- package/src/api-wrapper/builders.ts +68 -43
- package/src/api-wrapper/create-client.ts +82 -62
- package/src/api-wrapper/errors.ts +10 -0
- package/src/api-wrapper/headers.ts +14 -1
- package/src/api-wrapper/redaction.ts +1 -0
- package/src/api-wrapper/transport.ts +34 -38
- package/src/api-wrapper/types.ts +1 -0
- package/src/cache/index.ts +228 -61
- 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/typedswitch/index.ts +80 -69
|
@@ -15,6 +15,7 @@ import type { PathParams, QueryParams } from './shared'
|
|
|
15
15
|
import type { ApiErrorContext } from './shared'
|
|
16
16
|
import {
|
|
17
17
|
buildHeaders,
|
|
18
|
+
buildPathParams,
|
|
18
19
|
buildQueryParams,
|
|
19
20
|
buildRequestBody,
|
|
20
21
|
buildUrl
|
|
@@ -51,6 +52,39 @@ function isActionInvokeOptionsOnly(
|
|
|
51
52
|
)
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function resolveActionInvocation(
|
|
56
|
+
action: AnyApiAction,
|
|
57
|
+
actionName: string,
|
|
58
|
+
first: unknown,
|
|
59
|
+
second?: ActionInvokeOptions
|
|
60
|
+
): { input: unknown; options: ActionInvokeOptions } {
|
|
61
|
+
if (!action.noRuntimeInput) {
|
|
62
|
+
return { input: first, options: second ?? {} }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (first === undefined || first === null) {
|
|
66
|
+
return { input: undefined, options: second ?? {} }
|
|
67
|
+
}
|
|
68
|
+
if (second !== undefined) {
|
|
69
|
+
return { input: undefined, options: second }
|
|
70
|
+
}
|
|
71
|
+
if (isActionInvokeOptionsOnly(first)) {
|
|
72
|
+
return { input: undefined, options: first }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new ConfigError(
|
|
76
|
+
`Action "${actionName}" does not accept input; pass only optional { signal, meta }`,
|
|
77
|
+
{
|
|
78
|
+
endpoint: actionName,
|
|
79
|
+
method: 'GET',
|
|
80
|
+
url: '',
|
|
81
|
+
attempt: 0,
|
|
82
|
+
maxRetries: 0,
|
|
83
|
+
elapsedMs: 0
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
54
88
|
function createApiClientImpl<
|
|
55
89
|
T extends Record<string, ApiEndpoint>,
|
|
56
90
|
A extends Record<string, ApiAction> = Record<string, never>
|
|
@@ -102,31 +136,12 @@ function createApiClientImpl<
|
|
|
102
136
|
|
|
103
137
|
baseContext.method = endpoint.method
|
|
104
138
|
|
|
105
|
-
let
|
|
106
|
-
let fullUrl: string
|
|
107
|
-
try {
|
|
108
|
-
const built = buildUrl(
|
|
109
|
-
endpoint,
|
|
110
|
-
rawInput.reqParams,
|
|
111
|
-
rawInput.reqQuery,
|
|
112
|
-
apiConfig.baseApiUrls,
|
|
113
|
-
apiConfig.defaultBaseApiUrl,
|
|
114
|
-
input.url,
|
|
115
|
-
(msg) => new ConfigError(msg, ctx())
|
|
116
|
-
)
|
|
117
|
-
finalUrl = built.finalUrl
|
|
118
|
-
fullUrl = built.fullUrl
|
|
119
|
-
} catch (e) {
|
|
120
|
-
if (e instanceof ApiError) throw await reportApiError(e, apiConfig)
|
|
121
|
-
throw e
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
baseContext.url = fullUrl
|
|
125
|
-
|
|
139
|
+
let pathParams: PathParams | undefined
|
|
126
140
|
let queryParams: Record<string, string>
|
|
127
141
|
let body: unknown
|
|
128
142
|
let headers: Record<string, string>
|
|
129
143
|
try {
|
|
144
|
+
pathParams = buildPathParams(endpoint, rawInput.reqParams, ctx)
|
|
130
145
|
queryParams = buildQueryParams(endpoint, rawInput.reqQuery, ctx)
|
|
131
146
|
body = buildRequestBody(endpoint, rawInput.reqBody, ctx)
|
|
132
147
|
headers = buildHeaders(
|
|
@@ -140,6 +155,25 @@ function createApiClientImpl<
|
|
|
140
155
|
throw e
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
let fullUrl: string
|
|
159
|
+
try {
|
|
160
|
+
const built = buildUrl({
|
|
161
|
+
endpoint,
|
|
162
|
+
params: pathParams,
|
|
163
|
+
queryParams,
|
|
164
|
+
baseApiUrls: apiConfig.baseApiUrls,
|
|
165
|
+
defaultBaseApiUrl: apiConfig.defaultBaseApiUrl,
|
|
166
|
+
url: input.url,
|
|
167
|
+
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
168
|
+
configError: (msg) => new ConfigError(msg, ctx())
|
|
169
|
+
})
|
|
170
|
+
fullUrl = built.fullUrl
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (e instanceof ApiError) throw await reportApiError(e, apiConfig)
|
|
173
|
+
throw e
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
baseContext.url = fullUrl
|
|
143
177
|
baseContext.requestHeaders = headers
|
|
144
178
|
baseContext.requestBody = body
|
|
145
179
|
|
|
@@ -163,11 +197,9 @@ function createApiClientImpl<
|
|
|
163
197
|
|
|
164
198
|
const config: RequestConfig = {
|
|
165
199
|
method: endpoint.method,
|
|
166
|
-
|
|
167
|
-
queryParams,
|
|
200
|
+
fullUrl,
|
|
168
201
|
body,
|
|
169
202
|
headers,
|
|
170
|
-
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
171
203
|
timeoutMs: input.timeoutMs ?? apiConfig.timeoutMs,
|
|
172
204
|
fetchFn: input.fetch ?? apiConfig.fetch ?? fetch,
|
|
173
205
|
userSignal: input.signal
|
|
@@ -178,8 +210,11 @@ function createApiClientImpl<
|
|
|
178
210
|
config,
|
|
179
211
|
maxRetries,
|
|
180
212
|
retryDelay: apiConfig.retryDelayMs ?? 300,
|
|
181
|
-
|
|
182
|
-
|
|
213
|
+
maxRetryDelayMs: apiConfig.maxRetryDelayMs,
|
|
214
|
+
validateFn: (response, attempt) =>
|
|
215
|
+
validateAndTransformResponse(endpoint, response, (over) =>
|
|
216
|
+
ctx({ attempt, ...over })
|
|
217
|
+
),
|
|
183
218
|
shouldRetry: apiConfig.shouldRetry ?? defaultShouldRetry,
|
|
184
219
|
logger: apiConfig.logger,
|
|
185
220
|
ctx,
|
|
@@ -192,11 +227,12 @@ function createApiClientImpl<
|
|
|
192
227
|
})
|
|
193
228
|
|
|
194
229
|
const responseTimeMs = elapsed()
|
|
195
|
-
const responseSizeMb =
|
|
230
|
+
const responseSizeMb =
|
|
231
|
+
result.validatedData.responseSizeBytes / (1024 * 1024)
|
|
196
232
|
const requestBodySizeMb = calculateSizeInMb(config.body)
|
|
197
233
|
|
|
198
234
|
return {
|
|
199
|
-
data: result.validatedData as z.infer<T[K]['resSchema']>,
|
|
235
|
+
data: result.validatedData.data as z.infer<T[K]['resSchema']>,
|
|
200
236
|
requestBodySizeMb,
|
|
201
237
|
responseSizeMb,
|
|
202
238
|
responseTimeMs,
|
|
@@ -264,26 +300,28 @@ function createApiClientImpl<
|
|
|
264
300
|
}
|
|
265
301
|
}
|
|
266
302
|
|
|
303
|
+
async function dispatchAction(
|
|
304
|
+
name: string,
|
|
305
|
+
first?: unknown,
|
|
306
|
+
second?: ActionInvokeOptions
|
|
307
|
+
): Promise<unknown> {
|
|
308
|
+
const action = actions[name]!
|
|
309
|
+
const { input, options } = resolveActionInvocation(
|
|
310
|
+
action,
|
|
311
|
+
name,
|
|
312
|
+
first,
|
|
313
|
+
second
|
|
314
|
+
)
|
|
315
|
+
return invokeAction(name, input, options)
|
|
316
|
+
}
|
|
317
|
+
|
|
267
318
|
const request = (async (
|
|
268
319
|
key: string,
|
|
269
320
|
input?: unknown,
|
|
270
321
|
options?: ActionInvokeOptions
|
|
271
322
|
): Promise<unknown> => {
|
|
272
323
|
if (key in actions) {
|
|
273
|
-
|
|
274
|
-
if (action.noRuntimeInput) {
|
|
275
|
-
if (input === undefined || input === null) {
|
|
276
|
-
return invokeAction(key, undefined, options ?? {})
|
|
277
|
-
}
|
|
278
|
-
if (options !== undefined) {
|
|
279
|
-
return invokeAction(key, undefined, options)
|
|
280
|
-
}
|
|
281
|
-
if (isActionInvokeOptionsOnly(input)) {
|
|
282
|
-
return invokeAction(key, undefined, input)
|
|
283
|
-
}
|
|
284
|
-
return invokeAction(key, undefined, {})
|
|
285
|
-
}
|
|
286
|
-
return invokeAction(key, input, options)
|
|
324
|
+
return dispatchAction(key, input, options)
|
|
287
325
|
}
|
|
288
326
|
return invokeEndpoint(
|
|
289
327
|
key as keyof T,
|
|
@@ -305,26 +343,8 @@ function createApiClientImpl<
|
|
|
305
343
|
)
|
|
306
344
|
}
|
|
307
345
|
if (prop in actions) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return (
|
|
311
|
-
first?: unknown,
|
|
312
|
-
second?: ActionInvokeOptions
|
|
313
|
-
): Promise<unknown> => {
|
|
314
|
-
if (first === undefined || first === null) {
|
|
315
|
-
return invokeAction(prop, undefined, second ?? {})
|
|
316
|
-
}
|
|
317
|
-
if (second !== undefined) {
|
|
318
|
-
return invokeAction(prop, undefined, second)
|
|
319
|
-
}
|
|
320
|
-
if (isActionInvokeOptionsOnly(first)) {
|
|
321
|
-
return invokeAction(prop, undefined, first)
|
|
322
|
-
}
|
|
323
|
-
return invokeAction(prop, undefined, {})
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return (input?: unknown, opt?: ActionInvokeOptions) =>
|
|
327
|
-
invokeAction(prop, input, opt)
|
|
346
|
+
return (first?: unknown, second?: ActionInvokeOptions) =>
|
|
347
|
+
dispatchAction(prop, first, second)
|
|
328
348
|
}
|
|
329
349
|
return undefined
|
|
330
350
|
}
|
|
@@ -219,3 +219,13 @@ export const isConfigError = (e: unknown): e is ConfigError =>
|
|
|
219
219
|
e instanceof ConfigError
|
|
220
220
|
export const isActionError = (e: unknown): e is ActionError =>
|
|
221
221
|
e instanceof ActionError
|
|
222
|
+
|
|
223
|
+
export function isNonRetryableApiError(error: unknown): boolean {
|
|
224
|
+
return (
|
|
225
|
+
error instanceof HttpResponseError ||
|
|
226
|
+
error instanceof ResponseValidationError ||
|
|
227
|
+
error instanceof RequestValidationError ||
|
|
228
|
+
error instanceof ConfigError ||
|
|
229
|
+
error instanceof RequestAbortedError
|
|
230
|
+
)
|
|
231
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ConfigError } from './errors'
|
|
2
|
+
|
|
1
3
|
const CANONICAL_HEADER_NAMES: Record<string, string> = {
|
|
2
4
|
'content-type': 'Content-Type',
|
|
3
5
|
authorization: 'Authorization',
|
|
@@ -49,11 +51,22 @@ export function hasJsonContentType(headers: Record<string, string>): boolean {
|
|
|
49
51
|
|
|
50
52
|
export function buildFetchBody(
|
|
51
53
|
body: unknown,
|
|
52
|
-
headers: Record<string, string
|
|
54
|
+
headers: Record<string, string>,
|
|
55
|
+
configError?: (msg: string) => ConfigError
|
|
53
56
|
): RequestInit['body'] {
|
|
54
57
|
if (body === undefined || body === null) return undefined
|
|
55
58
|
if (body instanceof FormData || body instanceof Blob) return body
|
|
56
59
|
if (hasJsonContentType(headers)) return JSON.stringify(body)
|
|
60
|
+
if (
|
|
61
|
+
typeof body === 'object' &&
|
|
62
|
+
(Object.getPrototypeOf(body) === Object.prototype ||
|
|
63
|
+
Object.getPrototypeOf(body) === null) &&
|
|
64
|
+
configError
|
|
65
|
+
) {
|
|
66
|
+
throw configError(
|
|
67
|
+
'Request body is a plain object but Content-Type is not application/json'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
57
70
|
return body as RequestInit['body']
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -9,10 +9,10 @@ import type { ApiErrorContext, HttpMethod } from './shared'
|
|
|
9
9
|
import {
|
|
10
10
|
ConfigError,
|
|
11
11
|
HttpResponseError,
|
|
12
|
+
isNonRetryableApiError,
|
|
12
13
|
NetworkError,
|
|
13
14
|
RequestAbortedError,
|
|
14
15
|
RequestTimeoutError,
|
|
15
|
-
RequestValidationError,
|
|
16
16
|
ResponseValidationError
|
|
17
17
|
} from './errors'
|
|
18
18
|
import {
|
|
@@ -24,11 +24,9 @@ import { makeJsonParseZodError } from './zod-issues'
|
|
|
24
24
|
|
|
25
25
|
export type RequestConfig = {
|
|
26
26
|
method: HttpMethod
|
|
27
|
-
|
|
28
|
-
queryParams: Record<string, string>
|
|
27
|
+
fullUrl: string
|
|
29
28
|
body: unknown
|
|
30
29
|
headers: Record<string, string>
|
|
31
|
-
doNotEncodeQueryParams?: boolean
|
|
32
30
|
timeoutMs?: number
|
|
33
31
|
fetchFn: typeof fetch
|
|
34
32
|
userSignal?: AbortSignal
|
|
@@ -53,13 +51,6 @@ export async function httpRequest(
|
|
|
53
51
|
config: RequestConfig,
|
|
54
52
|
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
|
|
55
53
|
): 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
54
|
const timeoutController = new AbortController()
|
|
64
55
|
const timeout =
|
|
65
56
|
config.timeoutMs === undefined
|
|
@@ -69,10 +60,14 @@ export async function httpRequest(
|
|
|
69
60
|
const signal = mergeSignals(timeoutController.signal, config.userSignal)
|
|
70
61
|
|
|
71
62
|
try {
|
|
72
|
-
return await config.fetchFn(fullUrl, {
|
|
63
|
+
return await config.fetchFn(config.fullUrl, {
|
|
73
64
|
method: config.method,
|
|
74
65
|
headers: config.headers,
|
|
75
|
-
body: buildFetchBody(
|
|
66
|
+
body: buildFetchBody(
|
|
67
|
+
config.body,
|
|
68
|
+
config.headers,
|
|
69
|
+
(msg) => new ConfigError(msg, ctx())
|
|
70
|
+
),
|
|
76
71
|
signal
|
|
77
72
|
})
|
|
78
73
|
} catch (error) {
|
|
@@ -85,6 +80,7 @@ export async function httpRequest(
|
|
|
85
80
|
}
|
|
86
81
|
throw new RequestAbortedError(ctx(), error)
|
|
87
82
|
}
|
|
83
|
+
if (error instanceof ConfigError) throw error
|
|
88
84
|
throw new NetworkError(
|
|
89
85
|
error instanceof Error ? error.message : String(error),
|
|
90
86
|
ctx(),
|
|
@@ -142,30 +138,30 @@ export function getRetryDelay(params: {
|
|
|
142
138
|
retryDelay: number | ((attempt: number) => number)
|
|
143
139
|
attempt: number
|
|
144
140
|
response?: Response
|
|
141
|
+
maxRetryDelayMs?: number
|
|
145
142
|
}): number {
|
|
143
|
+
const maxDelay = params.maxRetryDelayMs ?? 30_000
|
|
146
144
|
const { retryDelay, attempt, response } = params
|
|
147
145
|
if (response && (response.status === 429 || response.status === 503)) {
|
|
148
146
|
const retryAfter = response.headers.get('retry-after')
|
|
149
147
|
if (retryAfter) {
|
|
150
148
|
const seconds = Number(retryAfter)
|
|
151
|
-
if (Number.isFinite(seconds))
|
|
149
|
+
if (Number.isFinite(seconds)) {
|
|
150
|
+
return Math.min(Math.max(0, seconds * 1000), maxDelay)
|
|
151
|
+
}
|
|
152
152
|
const dateMs = Date.parse(retryAfter)
|
|
153
153
|
if (!Number.isNaN(dateMs)) {
|
|
154
|
-
return Math.max(0, dateMs - Date.now())
|
|
154
|
+
return Math.min(Math.max(0, dateMs - Date.now()), maxDelay)
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
const delayMs =
|
|
159
|
+
typeof retryDelay === 'function' ? retryDelay(attempt) : retryDelay
|
|
160
|
+
return Math.min(delayMs, maxDelay)
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
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
|
|
164
|
+
return !isNonRetryableApiError(error)
|
|
169
165
|
}
|
|
170
166
|
|
|
171
167
|
export function shouldAttemptRetryOnHttp(retryCtx: RetryContext): boolean {
|
|
@@ -178,7 +174,8 @@ type MakeRequestArgs<TValidated> = {
|
|
|
178
174
|
config: RequestConfig
|
|
179
175
|
maxRetries: number
|
|
180
176
|
retryDelay: number | ((attempt: number) => number)
|
|
181
|
-
|
|
177
|
+
maxRetryDelayMs?: number
|
|
178
|
+
validateFn: (response: Response, attempt: number) => Promise<TValidated>
|
|
182
179
|
shouldRetry: (context: RetryContext) => boolean
|
|
183
180
|
logger?: Pick<Console, 'debug'>
|
|
184
181
|
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
|
|
@@ -201,6 +198,7 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
201
198
|
config,
|
|
202
199
|
maxRetries,
|
|
203
200
|
retryDelay,
|
|
201
|
+
maxRetryDelayMs,
|
|
204
202
|
validateFn,
|
|
205
203
|
shouldRetry,
|
|
206
204
|
logger,
|
|
@@ -244,7 +242,8 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
244
242
|
const delayMs = getRetryDelay({
|
|
245
243
|
retryDelay,
|
|
246
244
|
attempt: retryCount,
|
|
247
|
-
response
|
|
245
|
+
response,
|
|
246
|
+
maxRetryDelayMs
|
|
248
247
|
})
|
|
249
248
|
await onRetry?.({
|
|
250
249
|
...retryCtx,
|
|
@@ -267,18 +266,12 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
267
266
|
}
|
|
268
267
|
|
|
269
268
|
return {
|
|
270
|
-
validatedData: await validateFn(response),
|
|
269
|
+
validatedData: await validateFn(response, retryCount),
|
|
271
270
|
retryCount,
|
|
272
271
|
httpStatus: response.status
|
|
273
272
|
}
|
|
274
273
|
} catch (error) {
|
|
275
|
-
if (
|
|
276
|
-
error instanceof HttpResponseError ||
|
|
277
|
-
error instanceof ResponseValidationError ||
|
|
278
|
-
error instanceof RequestValidationError ||
|
|
279
|
-
error instanceof ConfigError ||
|
|
280
|
-
error instanceof RequestAbortedError
|
|
281
|
-
) {
|
|
274
|
+
if (isNonRetryableApiError(error)) {
|
|
282
275
|
throw error
|
|
283
276
|
}
|
|
284
277
|
|
|
@@ -297,7 +290,8 @@ export async function makeRequestWithRetry<TValidated>(
|
|
|
297
290
|
const delayMs = getRetryDelay({
|
|
298
291
|
retryDelay,
|
|
299
292
|
attempt: retryCount,
|
|
300
|
-
response: undefined
|
|
293
|
+
response: undefined,
|
|
294
|
+
maxRetryDelayMs
|
|
301
295
|
})
|
|
302
296
|
await onRetry?.({
|
|
303
297
|
...retryCtx,
|
|
@@ -320,11 +314,12 @@ export async function validateAndTransformResponse<T extends z.ZodType>(
|
|
|
320
314
|
>,
|
|
321
315
|
response: Response,
|
|
322
316
|
ctx: (over?: Partial<ApiErrorContext>) => ApiErrorContext
|
|
323
|
-
): Promise<z.infer<T
|
|
317
|
+
): Promise<{ data: z.infer<T>; responseSizeBytes: number }> {
|
|
324
318
|
const responseHeaders = getHeadersAsObject(response.headers)
|
|
319
|
+
const text = await response.text()
|
|
320
|
+
const responseSizeBytes = new TextEncoder().encode(text).length
|
|
325
321
|
let responseData: unknown
|
|
326
322
|
if (hasJsonContentType(responseHeaders)) {
|
|
327
|
-
const text = await response.text()
|
|
328
323
|
try {
|
|
329
324
|
responseData = text === '' ? undefined : JSON.parse(text)
|
|
330
325
|
} catch (error) {
|
|
@@ -335,13 +330,14 @@ export async function validateAndTransformResponse<T extends z.ZodType>(
|
|
|
335
330
|
)
|
|
336
331
|
}
|
|
337
332
|
} else {
|
|
338
|
-
responseData =
|
|
333
|
+
responseData = text
|
|
339
334
|
}
|
|
340
335
|
const transformedData = endpoint.resFormatter
|
|
341
336
|
? endpoint.resFormatter(responseData, responseHeaders)
|
|
342
337
|
: responseData
|
|
343
338
|
|
|
344
|
-
|
|
339
|
+
const data = await validateResponse(endpoint.resSchema, transformedData, ctx)
|
|
340
|
+
return { data, responseSizeBytes }
|
|
345
341
|
}
|
|
346
342
|
|
|
347
343
|
export function defaultShouldRetry(ctx: RetryContext): boolean {
|
package/src/api-wrapper/types.ts
CHANGED
|
@@ -225,6 +225,7 @@ export interface ApiClientConfig<
|
|
|
225
225
|
timeoutMs?: number
|
|
226
226
|
retries?: number
|
|
227
227
|
retryDelayMs?: number | ((attempt: number) => number)
|
|
228
|
+
maxRetryDelayMs?: number
|
|
228
229
|
shouldRetry?: (context: RetryContext) => boolean
|
|
229
230
|
logger?: Pick<Console, 'debug' | 'error'>
|
|
230
231
|
fetch?: typeof fetch
|