rqlite-js 2.6.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.
Files changed (98) hide show
  1. package/README.md +207 -29
  2. package/dist/index.cjs +904 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +329 -0
  5. package/dist/index.d.ts +329 -0
  6. package/dist/index.js +865 -0
  7. package/dist/index.js.map +1 -0
  8. package/docs/MIGRATION-v2-to-v3.md +71 -0
  9. package/package.json +56 -35
  10. package/src/api/backup/index.ts +69 -0
  11. package/src/api/backup-formats.ts +8 -0
  12. package/src/api/client/index.ts +97 -0
  13. package/src/api/consistency-levels.ts +10 -0
  14. package/src/api/data/index.ts +87 -0
  15. package/src/api/paths.ts +13 -0
  16. package/src/api/query-params.ts +15 -0
  17. package/src/api/results/data-result-error.ts +41 -0
  18. package/src/api/results/data-result.ts +91 -0
  19. package/src/api/results/data-results.ts +91 -0
  20. package/src/api/results/index.ts +7 -0
  21. package/src/api/results/result-fields.ts +14 -0
  22. package/src/api/status/index.ts +53 -0
  23. package/src/http-request/auth-keys.ts +14 -0
  24. package/src/http-request/content-types.ts +4 -0
  25. package/src/http-request/errors.ts +17 -0
  26. package/src/http-request/http-headers.ts +19 -0
  27. package/src/http-request/http-methods.ts +12 -0
  28. package/src/http-request/index.ts +676 -0
  29. package/src/http-request/retryable.ts +41 -0
  30. package/src/index.ts +38 -0
  31. package/lib/api/backup/index.js +0 -72
  32. package/lib/api/backup/index.js.map +0 -1
  33. package/lib/api/backup/test.unit.js +0 -45
  34. package/lib/api/backup/test.unit.js.map +0 -1
  35. package/lib/api/backup-formats.js +0 -16
  36. package/lib/api/backup-formats.js.map +0 -1
  37. package/lib/api/client/index.js +0 -105
  38. package/lib/api/client/index.js.map +0 -1
  39. package/lib/api/client/test.unit.js +0 -53
  40. package/lib/api/client/test.unit.js.map +0 -1
  41. package/lib/api/consistency-levels.js +0 -30
  42. package/lib/api/consistency-levels.js.map +0 -1
  43. package/lib/api/data/index.js +0 -108
  44. package/lib/api/data/index.js.map +0 -1
  45. package/lib/api/data/test.unit.js +0 -98
  46. package/lib/api/data/test.unit.js.map +0 -1
  47. package/lib/api/paths.js +0 -33
  48. package/lib/api/paths.js.map +0 -1
  49. package/lib/api/query-params.js +0 -43
  50. package/lib/api/query-params.js.map +0 -1
  51. package/lib/api/results/data-result-error.js +0 -62
  52. package/lib/api/results/data-result-error.js.map +0 -1
  53. package/lib/api/results/data-result.js +0 -141
  54. package/lib/api/results/data-result.js.map +0 -1
  55. package/lib/api/results/data-results.js +0 -142
  56. package/lib/api/results/data-results.js.map +0 -1
  57. package/lib/api/results/index.js +0 -8
  58. package/lib/api/results/index.js.map +0 -1
  59. package/lib/api/results/result-fields.js +0 -47
  60. package/lib/api/results/result-fields.js.map +0 -1
  61. package/lib/api/results/test.unit.js +0 -238
  62. package/lib/api/results/test.unit.js.map +0 -1
  63. package/lib/api/status/index.js +0 -67
  64. package/lib/api/status/index.js.map +0 -1
  65. package/lib/api/status/test.unit.js +0 -57
  66. package/lib/api/status/test.unit.js.map +0 -1
  67. package/lib/http-request/auth-keys.js +0 -15
  68. package/lib/http-request/auth-keys.js.map +0 -1
  69. package/lib/http-request/content-types.js +0 -5
  70. package/lib/http-request/content-types.js.map +0 -1
  71. package/lib/http-request/errors.js +0 -17
  72. package/lib/http-request/errors.js.map +0 -1
  73. package/lib/http-request/http-headers.js +0 -20
  74. package/lib/http-request/http-headers.js.map +0 -1
  75. package/lib/http-request/http-methods.js +0 -40
  76. package/lib/http-request/http-methods.js.map +0 -1
  77. package/lib/http-request/index.js +0 -764
  78. package/lib/http-request/index.js.map +0 -1
  79. package/lib/http-request/retryable.js +0 -42
  80. package/lib/http-request/retryable.js.map +0 -1
  81. package/lib/http-request/test.unit.js +0 -560
  82. package/lib/http-request/test.unit.js.map +0 -1
  83. package/lib/index.js +0 -39
  84. package/lib/index.js.map +0 -1
  85. package/lib/test/api-data-execute-nock.js +0 -142
  86. package/lib/test/api-data-execute-nock.js.map +0 -1
  87. package/lib/test/api-data-query-nock.js +0 -210
  88. package/lib/test/api-data-query-nock.js.map +0 -1
  89. package/lib/test/backups/backup-nock.js +0 -29
  90. package/lib/test/backups/backup-nock.js.map +0 -1
  91. package/lib/test/backups/restore-nock.js +0 -30
  92. package/lib/test/backups/restore-nock.js.map +0 -1
  93. package/lib/test/env.js +0 -15
  94. package/lib/test/env.js.map +0 -1
  95. package/lib/test/unit/index.unit.js +0 -27
  96. package/lib/test/unit/index.unit.js.map +0 -1
  97. package/lib/test.integration.js +0 -238
  98. package/lib/test.integration.js.map +0 -1
@@ -0,0 +1,676 @@
1
+ /**
2
+ * Plain HTTP client to be used when creating RQLite specific API HTTP clients
3
+ * @module http-request
4
+ * @see https://rqlite.io/docs/api/api/
5
+ */
6
+ import { Readable } from 'node:stream'
7
+ import type { Agent as HttpAgent } from 'node:http'
8
+ import type { Agent as HttpsAgent } from 'node:https'
9
+ import {
10
+ HTTP_METHOD_GET,
11
+ HTTP_METHOD_POST,
12
+ } from './http-methods.js'
13
+ import {
14
+ CONTENT_TYPE_APPLICATION_JSON,
15
+ } from './content-types.js'
16
+ import { ERROR_HTTP_REQUEST_MAX_REDIRECTS } from './errors.js'
17
+ import {
18
+ RETRYABLE_ERROR_CODES,
19
+ RETRYABLE_HTTP_METHODS,
20
+ RETRYABLE_STATUS_CODES,
21
+ } from './retryable.js'
22
+ import { AUTH_PASSWORD, AUTH_USERNAME } from './auth-keys.js'
23
+ import {
24
+ HTTP_HEADER_ACCEPT,
25
+ HTTP_HEADER_CONTENT_TYPE,
26
+ HTTP_HEADER_LOCATION,
27
+ } from './http-headers.js'
28
+
29
+ export type HttpAuthentication = {
30
+ username?: string
31
+ password?: string
32
+ }
33
+
34
+ export type HttpRequestOptions = {
35
+ auth?: HttpAuthentication
36
+ body?: unknown
37
+ headers?: Record<string, string>
38
+ httpMethod?: string
39
+ query?: Record<string, unknown>
40
+ stream?: boolean
41
+ timeout?: number
42
+ uri?: string
43
+ useLeader?: boolean
44
+ retries?: number
45
+ maxRedirects?: number
46
+ attempt?: number
47
+ retryAttempt?: number
48
+ redirectAttempt?: number
49
+ attemptHostIndex?: number
50
+ exponentailBackoffBase?: number
51
+ exponentailBackoffExponent?: number
52
+ json?: boolean
53
+ httpAgent?: HttpAgent | DispatcherLike
54
+ httpsAgent?: HttpsAgent | DispatcherLike
55
+ }
56
+
57
+ /** Minimal undici/Node fetch dispatcher shape */
58
+ export type DispatcherLike = {
59
+ dispatch?: (...args: unknown[]) => unknown
60
+ close?: () => Promise<void>
61
+ destroy?: () => Promise<void>
62
+ }
63
+
64
+ export type HttpResponse = {
65
+ status: number
66
+ body: unknown
67
+ }
68
+
69
+ export type HttpRequestConstructorOptions = {
70
+ authentication?: HttpAuthentication
71
+ activeHostRoundRobin?: boolean
72
+ httpAgent?: HttpAgent | DispatcherLike
73
+ httpsAgent?: HttpsAgent | DispatcherLike
74
+ retryableErrorCodes?: Set<string> | string[]
75
+ retryableStatusCodes?: Set<number> | number[]
76
+ retryableHttpMethods?: Set<string> | string[]
77
+ exponentailBackoffBase?: number
78
+ exponentailBackoffExponent?: number
79
+ }
80
+
81
+ /**
82
+ * The default timeout value
83
+ */
84
+ export const DEAULT_TIMEOUT = 30000
85
+
86
+ /**
87
+ * The default to retry a request using the next host in the chain
88
+ */
89
+ export const DEAULT_RETRY_DELAY = 30000
90
+
91
+ /**
92
+ * Create default header for all HTTP requests
93
+ */
94
+ export function createDefaultHeaders (headers: Record<string, string> = {}): Record<string, string> {
95
+ const accept = headers[HTTP_HEADER_ACCEPT] || CONTENT_TYPE_APPLICATION_JSON
96
+ return { ...headers, [HTTP_HEADER_ACCEPT]: accept }
97
+ }
98
+
99
+ /**
100
+ * Clean the request path remove / from the beginning
101
+ */
102
+ function cleanPath (path: string): string {
103
+ return String(path).replace(/^\//, '')
104
+ }
105
+
106
+ /**
107
+ * Serialize query params similar to qs arrayFormat: 'brackets'
108
+ */
109
+ export function stringifyQuery (params: Record<string, unknown> = {}): string {
110
+ const search = new URLSearchParams()
111
+ Object.entries(params).forEach(([key, value]) => {
112
+ if (typeof value === 'undefined') {
113
+ return
114
+ }
115
+ if (Array.isArray(value)) {
116
+ value.forEach((entry) => {
117
+ search.append(`${key}[]`, String(entry))
118
+ })
119
+ return
120
+ }
121
+ search.append(key, String(value))
122
+ })
123
+ return search.toString()
124
+ }
125
+
126
+ function basicAuthHeader (username: string, password: string): string {
127
+ const token = Buffer.from(`${username}:${password}`).toString('base64')
128
+ return `Basic ${token}`
129
+ }
130
+
131
+ function getErrorCode (error: unknown): string | undefined {
132
+ if (!error || typeof error !== 'object') {
133
+ return undefined
134
+ }
135
+ const err = error as { code?: string, cause?: { code?: string }, name?: string }
136
+ if (typeof err.code === 'string') {
137
+ return err.code
138
+ }
139
+ if (err.cause && typeof err.cause.code === 'string') {
140
+ return err.cause.code
141
+ }
142
+ if (err.name === 'AbortError' || err.name === 'TimeoutError') {
143
+ return 'ETIMEDOUT'
144
+ }
145
+ return undefined
146
+ }
147
+
148
+ /**
149
+ * Returns the next wait interval, in milliseconds, using an exponential
150
+ * backoff algorithm.
151
+ */
152
+ export function getWaitTimeExponential (attempt = 0, base = 100, exponent = 2): number {
153
+ if (attempt === 0) {
154
+ return 0
155
+ }
156
+ return (exponent ** attempt) * base
157
+ }
158
+
159
+ /**
160
+ * Generic HTTP Request class which all RQLiteJS clients should extend
161
+ */
162
+ export default class HttpRequest {
163
+ activeHostIndex = 0
164
+
165
+ activeHostRoundRobin = true
166
+
167
+ absoluteUriPattern = /^https?:\/\//
168
+
169
+ hosts: string[] = []
170
+
171
+ httpAgent?: HttpAgent | DispatcherLike
172
+
173
+ httpsAgent?: HttpsAgent | DispatcherLike
174
+
175
+ leaderHostIndex = 0
176
+
177
+ retryableErrorCodes: Set<string> = RETRYABLE_ERROR_CODES
178
+
179
+ retryableStatusCodes: Set<number> = RETRYABLE_STATUS_CODES
180
+
181
+ retryableHttpMethods: Set<string> = RETRYABLE_HTTP_METHODS
182
+
183
+ exponentailBackoffBase = 100
184
+
185
+ exponentailBackoffExponent = 1.1
186
+
187
+ authentication = new Map<string, string>()
188
+
189
+ constructor (hosts: string[] | string, options: HttpRequestConstructorOptions = {}) {
190
+ this.setHosts(hosts)
191
+ if (this.getTotalHosts() === 0) {
192
+ throw new Error('At least one host must be provided')
193
+ }
194
+ const {
195
+ activeHostRoundRobin = true,
196
+ httpAgent,
197
+ httpsAgent,
198
+ retryableErrorCodes,
199
+ retryableStatusCodes,
200
+ retryableHttpMethods,
201
+ exponentailBackoffExponent,
202
+ exponentailBackoffBase,
203
+ authentication,
204
+ } = options
205
+ if (typeof authentication === 'object') {
206
+ const { username, password } = authentication
207
+ if (username) {
208
+ this.authentication.set(AUTH_USERNAME, username)
209
+ }
210
+ if (password) {
211
+ this.authentication.set(AUTH_PASSWORD, password)
212
+ }
213
+ }
214
+ if (typeof activeHostRoundRobin !== 'undefined') {
215
+ this.setActiveHostRoundRobin(activeHostRoundRobin)
216
+ }
217
+ if (typeof httpAgent !== 'undefined') {
218
+ this.setHttpAgent(httpAgent)
219
+ }
220
+ if (typeof httpsAgent !== 'undefined') {
221
+ this.setHttpsAgent(httpsAgent)
222
+ }
223
+ if (retryableErrorCodes instanceof Set || Array.isArray(retryableErrorCodes)) {
224
+ this.setRetryableErrorCodes(
225
+ Array.isArray(retryableErrorCodes) ? new Set(retryableErrorCodes) : retryableErrorCodes,
226
+ )
227
+ }
228
+ if (retryableStatusCodes instanceof Set || Array.isArray(retryableStatusCodes)) {
229
+ this.setRetryableStatusCodes(
230
+ Array.isArray(retryableStatusCodes) ? new Set(retryableStatusCodes) : retryableStatusCodes,
231
+ )
232
+ }
233
+ if (retryableHttpMethods instanceof Set || Array.isArray(retryableHttpMethods)) {
234
+ this.setRetryableHttpMethods(
235
+ Array.isArray(retryableHttpMethods) ? new Set(retryableHttpMethods) : retryableHttpMethods,
236
+ )
237
+ }
238
+ if (Number.isFinite(exponentailBackoffBase)) {
239
+ this.setExponentailBackoffBase(exponentailBackoffBase as number)
240
+ }
241
+ if (Number.isFinite(exponentailBackoffExponent)) {
242
+ this.setExponentailBackoffExponent(exponentailBackoffExponent as number)
243
+ }
244
+ }
245
+
246
+ setAuthentication (authentication: HttpAuthentication = {}): void {
247
+ const { username, password } = authentication
248
+ if (username) {
249
+ this.authentication.set(AUTH_USERNAME, username)
250
+ }
251
+ if (password) {
252
+ this.authentication.set(AUTH_PASSWORD, password)
253
+ }
254
+ }
255
+
256
+ setExponentailBackoffBase (exponentailBackoffBase: number): void {
257
+ this.exponentailBackoffBase = exponentailBackoffBase
258
+ }
259
+
260
+ getExponentailBackoffBase (): number {
261
+ return this.exponentailBackoffBase
262
+ }
263
+
264
+ setExponentailBackoffExponent (exponentailBackoffExponent: number): void {
265
+ this.exponentailBackoffExponent = exponentailBackoffExponent
266
+ }
267
+
268
+ getExponentailBackoffExponent (): number {
269
+ return this.exponentailBackoffExponent
270
+ }
271
+
272
+ setRetryableErrorCodes (retryableErrorCodes: Set<string>): void {
273
+ this.retryableErrorCodes = retryableErrorCodes
274
+ }
275
+
276
+ getRetryableErrorCodes (): Set<string> {
277
+ return this.retryableErrorCodes
278
+ }
279
+
280
+ setRetryableStatusCodes (retryableStatusCodes: Set<number>): void {
281
+ this.retryableStatusCodes = retryableStatusCodes
282
+ }
283
+
284
+ getRetryableStatusCodes (): Set<number> {
285
+ return this.retryableStatusCodes
286
+ }
287
+
288
+ setRetryableHttpMethods (retryableHttpMethods: Set<string>): void {
289
+ this.retryableHttpMethods = retryableHttpMethods
290
+ }
291
+
292
+ getRetryableHttpMethods (): Set<string> {
293
+ return this.retryableHttpMethods
294
+ }
295
+
296
+ setHttpAgent (httpAgent: HttpAgent | DispatcherLike): void {
297
+ this.httpAgent = httpAgent
298
+ }
299
+
300
+ getHttpAgent (): HttpAgent | DispatcherLike | undefined {
301
+ return this.httpAgent
302
+ }
303
+
304
+ setHttpsAgent (httpsAgent: HttpsAgent | DispatcherLike): void {
305
+ this.httpsAgent = httpsAgent
306
+ }
307
+
308
+ getHttpsAgent (): HttpsAgent | DispatcherLike | undefined {
309
+ return this.httpsAgent
310
+ }
311
+
312
+ setHosts (hosts: string[] | string): void {
313
+ const list = !Array.isArray(hosts) ? String(hosts).split(',') : hosts
314
+ this.hosts = list.reduce<string[]>((acc, v) => {
315
+ const host = String(v).trim().replace(/\/$/, '')
316
+ if (!host) {
317
+ return acc
318
+ }
319
+ return acc.concat(host)
320
+ }, [])
321
+ }
322
+
323
+ getHosts (): string[] {
324
+ return this.hosts
325
+ }
326
+
327
+ findHostIndex (host: string): number {
328
+ let parsedHostToFind: URL
329
+ try {
330
+ parsedHostToFind = new URL(host)
331
+ } catch {
332
+ return -1
333
+ }
334
+ return this.getHosts().findIndex((v) => {
335
+ try {
336
+ const parsedHost = new URL(v)
337
+ return (['hostname', 'protocol', 'port', 'pathname'] as const).every(
338
+ (field) => parsedHostToFind[field] === parsedHost[field],
339
+ )
340
+ } catch {
341
+ return false
342
+ }
343
+ })
344
+ }
345
+
346
+ getActiveHost (useLeader?: boolean): string {
347
+ const activeHostIndex = useLeader ? this.getLeaderHostIndex() : this.getActiveHostIndex()
348
+ return this.getHosts()[activeHostIndex]
349
+ }
350
+
351
+ setActiveHostIndex (activeHostIndex: number): number {
352
+ if (!Number.isFinite(activeHostIndex)) {
353
+ throw new Error('The activeHostIndex should be a finite number')
354
+ }
355
+ const totalHosts = this.getTotalHosts()
356
+ if (activeHostIndex < 0) {
357
+ this.activeHostIndex = 0
358
+ } else if (activeHostIndex >= totalHosts) {
359
+ this.activeHostIndex = totalHosts - 1
360
+ } else {
361
+ this.activeHostIndex = activeHostIndex
362
+ }
363
+ return this.activeHostIndex
364
+ }
365
+
366
+ getLeaderHostIndex (): number {
367
+ return this.leaderHostIndex
368
+ }
369
+
370
+ setLeaderHostIndex (leaderHostIndex: number): number {
371
+ if (!Number.isFinite(leaderHostIndex)) {
372
+ throw new Error('The leaderHostIndex should be a finite number')
373
+ }
374
+ const totalHosts = this.getTotalHosts()
375
+ if (leaderHostIndex < 0) {
376
+ this.leaderHostIndex = 0
377
+ } else if (leaderHostIndex > totalHosts) {
378
+ this.leaderHostIndex = totalHosts - 1
379
+ } else {
380
+ this.leaderHostIndex = leaderHostIndex
381
+ }
382
+ return this.leaderHostIndex
383
+ }
384
+
385
+ getActiveHostIndex (): number {
386
+ return this.activeHostIndex
387
+ }
388
+
389
+ setActiveHostRoundRobin (activeHostRoundRobin: boolean): void {
390
+ if (typeof activeHostRoundRobin !== 'boolean') {
391
+ throw new Error('The activeHostRoundRobin argument must be boolean')
392
+ }
393
+ this.activeHostRoundRobin = activeHostRoundRobin
394
+ }
395
+
396
+ getActiveHostRoundRobin (): boolean {
397
+ return this.activeHostRoundRobin
398
+ }
399
+
400
+ getNextActiveHostIndex (activeHostIndex = this.getActiveHostIndex()): number {
401
+ const totalHosts = this.getTotalHosts()
402
+ const nextIndex = activeHostIndex + 1
403
+ if (totalHosts === nextIndex) {
404
+ return 0
405
+ }
406
+ return nextIndex
407
+ }
408
+
409
+ setNextActiveHostIndex (): void {
410
+ if (!this.getActiveHostRoundRobin()) {
411
+ return
412
+ }
413
+ const totalHosts = this.getTotalHosts()
414
+ if (this.getActiveHostRoundRobin() && totalHosts <= 1) {
415
+ return
416
+ }
417
+ this.setActiveHostIndex(this.getNextActiveHostIndex())
418
+ }
419
+
420
+ getTotalHosts (): number {
421
+ return this.getHosts().length
422
+ }
423
+
424
+ uriIsAbsolute (uri: string): boolean {
425
+ return this.absoluteUriPattern.test(uri)
426
+ }
427
+
428
+ requestIsRetryable (options: {
429
+ statusCode?: number
430
+ errorCode?: string
431
+ httpMethod?: string
432
+ } = {}): boolean {
433
+ const {
434
+ statusCode,
435
+ errorCode,
436
+ httpMethod,
437
+ } = options
438
+ if (!httpMethod || !this.getRetryableHttpMethods().has(httpMethod)) {
439
+ return false
440
+ }
441
+ if (statusCode && this.getRetryableStatusCodes().has(statusCode)) {
442
+ return true
443
+ }
444
+ if (errorCode && this.getRetryableErrorCodes().has(errorCode)) {
445
+ return true
446
+ }
447
+ return false
448
+ }
449
+
450
+ /**
451
+ * Perform an HTTP request using the provided options
452
+ */
453
+ async fetch (options: HttpRequestOptions = {}): Promise<HttpResponse | Readable> {
454
+ const {
455
+ body,
456
+ headers = {},
457
+ httpMethod = HTTP_METHOD_GET,
458
+ query,
459
+ stream = false,
460
+ timeout = DEAULT_TIMEOUT,
461
+ useLeader = false,
462
+ retries = this.getTotalHosts() * 3,
463
+ maxRedirects = 10,
464
+ attempt = 0,
465
+ retryAttempt = 0,
466
+ redirectAttempt = 0,
467
+ attemptHostIndex,
468
+ exponentailBackoffExponent = this.getExponentailBackoffExponent(),
469
+ exponentailBackoffBase = this.getExponentailBackoffBase(),
470
+ httpAgent = this.getHttpAgent(),
471
+ httpsAgent = this.getHttpsAgent(),
472
+ } = options
473
+
474
+ const activeHost = Number.isFinite(attemptHostIndex)
475
+ ? this.getHosts()[attemptHostIndex as number]
476
+ : this.getActiveHost(useLeader)
477
+
478
+ let { uri } = options
479
+ if (!uri) {
480
+ throw new Error('The uri option is required')
481
+ }
482
+ uri = this.uriIsAbsolute(uri) ? uri : `${activeHost}/${cleanPath(uri)}`
483
+
484
+ const requestHeaders = createDefaultHeaders(headers)
485
+ if (this.authentication.size) {
486
+ const username = this.authentication.get(AUTH_USERNAME) || ''
487
+ const password = this.authentication.get(AUTH_PASSWORD) || ''
488
+ requestHeaders.Authorization = basicAuthHeader(username, password)
489
+ }
490
+
491
+ const url = new URL(uri)
492
+ if (query && typeof query === 'object') {
493
+ const serialized = stringifyQuery(query as Record<string, unknown>)
494
+ if (serialized) {
495
+ url.search = serialized
496
+ }
497
+ }
498
+
499
+ const controller = new AbortController()
500
+ const timeoutId = setTimeout(() => controller.abort(), timeout)
501
+
502
+ const fetchInit: RequestInit & { dispatcher?: unknown } = {
503
+ method: httpMethod,
504
+ headers: requestHeaders,
505
+ redirect: 'manual',
506
+ signal: controller.signal,
507
+ }
508
+
509
+ // Only pass undici-compatible dispatchers. Node http(s).Agent is still
510
+ // accepted via options for API compatibility but is not a fetch dispatcher.
511
+ const agent = url.protocol === 'https:' ? httpsAgent : httpAgent
512
+ if (agent && typeof (agent as DispatcherLike).dispatch === 'function') {
513
+ // undici Dispatcher; cast avoids Node http.Agent type conflict
514
+ const init = fetchInit as { dispatcher?: unknown }
515
+ init.dispatcher = agent
516
+ }
517
+
518
+ if (typeof body !== 'undefined' && body !== null) {
519
+ if (Buffer.isBuffer(body) || typeof body === 'string') {
520
+ fetchInit.body = body as string | Buffer
521
+ } else {
522
+ fetchInit.body = JSON.stringify(body)
523
+ if (!requestHeaders[HTTP_HEADER_ACCEPT]) {
524
+ requestHeaders[HTTP_HEADER_ACCEPT] = CONTENT_TYPE_APPLICATION_JSON
525
+ }
526
+ if (!(HTTP_HEADER_CONTENT_TYPE in requestHeaders)
527
+ && !Object.keys(requestHeaders).some((k) => k.toLowerCase() === 'content-type')) {
528
+ requestHeaders[HTTP_HEADER_CONTENT_TYPE] = CONTENT_TYPE_APPLICATION_JSON
529
+ }
530
+ fetchInit.headers = requestHeaders
531
+ }
532
+ }
533
+
534
+ const retryOrRedirect = async (
535
+ responseStatus: number | undefined,
536
+ responseHeaders: Headers | Record<string, string> | undefined,
537
+ errorCode: string | undefined,
538
+ error: unknown,
539
+ ): Promise<HttpResponse | Readable> => {
540
+ const retryable = this.requestIsRetryable({
541
+ statusCode: responseStatus,
542
+ errorCode,
543
+ httpMethod,
544
+ })
545
+
546
+ let nextAttemptHostIndex = Number.isFinite(attemptHostIndex)
547
+ ? (attemptHostIndex as number)
548
+ : this.getActiveHostIndex()
549
+ nextAttemptHostIndex += 1
550
+ if (nextAttemptHostIndex === this.getTotalHosts()) {
551
+ nextAttemptHostIndex = 0
552
+ }
553
+
554
+ if (responseStatus === 301 || responseStatus === 302) {
555
+ if (redirectAttempt >= maxRedirects) {
556
+ throw new ERROR_HTTP_REQUEST_MAX_REDIRECTS(
557
+ `The maximum number of redirects ${maxRedirects} has been reached`,
558
+ )
559
+ }
560
+ let location: string | undefined
561
+ if (responseHeaders && typeof (responseHeaders as Headers).get === 'function') {
562
+ location = (responseHeaders as Headers).get(HTTP_HEADER_LOCATION)
563
+ || (responseHeaders as Headers).get(HTTP_HEADER_LOCATION.toLowerCase())
564
+ || undefined
565
+ } else if (responseHeaders && typeof responseHeaders === 'object') {
566
+ const headerMap = responseHeaders as Record<string, string>
567
+ location = headerMap[HTTP_HEADER_LOCATION.toLowerCase()]
568
+ || headerMap[HTTP_HEADER_LOCATION]
569
+ }
570
+ if (useLeader && location) {
571
+ const newLeaderHostIndex = this.findHostIndex(location)
572
+ if (newLeaderHostIndex > -1) {
573
+ this.setLeaderHostIndex(newLeaderHostIndex)
574
+ }
575
+ }
576
+ return this.fetch({
577
+ ...options,
578
+ uri: location,
579
+ attempt: attempt + 1,
580
+ redirectAttempt: redirectAttempt + 1,
581
+ attemptHostIndex: nextAttemptHostIndex,
582
+ })
583
+ }
584
+
585
+ if (retryable && retryAttempt < retries) {
586
+ const waitTime = getWaitTimeExponential(
587
+ retryAttempt,
588
+ exponentailBackoffBase,
589
+ exponentailBackoffExponent,
590
+ )
591
+ await new Promise<void>((resolve) => {
592
+ setTimeout(() => resolve(), waitTime)
593
+ })
594
+ return this.fetch({
595
+ ...options,
596
+ attempt: attempt + 1,
597
+ retryAttempt: retryAttempt + 1,
598
+ attemptHostIndex: nextAttemptHostIndex,
599
+ })
600
+ }
601
+
602
+ if (error instanceof Error) {
603
+ throw error
604
+ }
605
+ const wrapped = new Error(
606
+ (error as { message?: string })?.message || String(error),
607
+ )
608
+ Object.assign(wrapped, error)
609
+ throw wrapped
610
+ }
611
+
612
+ try {
613
+ const response = await fetch(url as unknown as string, fetchInit as RequestInit)
614
+ clearTimeout(timeoutId)
615
+
616
+ if (response.status === 301 || response.status === 302) {
617
+ return retryOrRedirect(response.status, response.headers, undefined, new Error(
618
+ `Redirect ${response.status}`,
619
+ ))
620
+ }
621
+
622
+ // Match axios: non-2xx responses reject (after retry logic when applicable)
623
+ if (response.status >= 400) {
624
+ return retryOrRedirect(
625
+ response.status,
626
+ response.headers,
627
+ undefined,
628
+ Object.assign(new Error(`HTTP ${response.status}`), {
629
+ response: {
630
+ status: response.status,
631
+ headers: Object.fromEntries(response.headers.entries()),
632
+ },
633
+ }),
634
+ )
635
+ }
636
+
637
+ if (stream) {
638
+ if (!response.body) {
639
+ return Readable.from([])
640
+ }
641
+ return Readable.fromWeb(response.body as import('stream/web').ReadableStream)
642
+ }
643
+
644
+ const contentType = response.headers.get('content-type') || ''
645
+ let responseBody: unknown
646
+ if (contentType.includes('application/json')) {
647
+ responseBody = await response.json()
648
+ } else {
649
+ responseBody = await response.text()
650
+ }
651
+
652
+ return {
653
+ body: responseBody,
654
+ status: response.status,
655
+ }
656
+ } catch (e) {
657
+ clearTimeout(timeoutId)
658
+ const errorCode = getErrorCode(e)
659
+ const err = e as { response?: { status?: number, headers?: Record<string, string> } }
660
+ const responseStatus = err.response?.status
661
+ const responseHeaders = err.response?.headers
662
+ return retryOrRedirect(responseStatus, responseHeaders, errorCode, e)
663
+ }
664
+ }
665
+
666
+ // Rest args keep subclass ApiClient get(path, sql, options) compatible (v2 JS shape)
667
+ async get (...args: any[]): Promise<HttpResponse | Readable> {
668
+ const options = (args[0] || {}) as HttpRequestOptions
669
+ return this.fetch({ ...options, httpMethod: HTTP_METHOD_GET })
670
+ }
671
+
672
+ async post (...args: any[]): Promise<HttpResponse | Readable> {
673
+ const options = (args[0] || {}) as HttpRequestOptions
674
+ return this.fetch({ ...options, httpMethod: HTTP_METHOD_POST })
675
+ }
676
+ }
@@ -0,0 +1,41 @@
1
+ import {
2
+ HTTP_METHOD_DELETE,
3
+ HTTP_METHOD_GET,
4
+ HTTP_METHOD_HEAD,
5
+ HTTP_METHOD_OPTIONS,
6
+ HTTP_METHOD_PATCH,
7
+ HTTP_METHOD_POST,
8
+ HTTP_METHOD_PUT,
9
+ } from './http-methods.js'
10
+
11
+ /**
12
+ * Error codes which can be retried
13
+ */
14
+ export const RETRYABLE_ERROR_CODES = new Set([
15
+ 'EADDRINUSE',
16
+ 'EAI_AGAIN',
17
+ 'ECONNREFUSED',
18
+ 'ECONNRESET',
19
+ 'ENETUNREACH',
20
+ 'ENOTFOUND',
21
+ 'EPIPE',
22
+ 'ETIMEDOUT',
23
+ ])
24
+
25
+ /**
26
+ * HTTP status codes which can be retried
27
+ */
28
+ export const RETRYABLE_STATUS_CODES = new Set([408, 413, 429, 500, 502, 503, 504, 521, 522, 524])
29
+
30
+ /**
31
+ * HTTP methods which can be retried
32
+ */
33
+ export const RETRYABLE_HTTP_METHODS = new Set([
34
+ HTTP_METHOD_DELETE,
35
+ HTTP_METHOD_GET,
36
+ HTTP_METHOD_HEAD,
37
+ HTTP_METHOD_OPTIONS,
38
+ HTTP_METHOD_PATCH,
39
+ HTTP_METHOD_POST,
40
+ HTTP_METHOD_PUT,
41
+ ])