1616
1717import {
1818 ApiResponse ,
19+ ApiResponseCredits ,
20+ ApiResponseThrottling ,
1921 BatchResult ,
2022 DefaultRequestHandler ,
2123 IpregistryRequestHandler ,
@@ -32,6 +34,12 @@ import { IpregistryOption } from './options.js'
3234
3335import { isApiError , LookupError } from './errors.js'
3436
37+ /**
38+ * The maximum number of IP addresses or ASNs the Ipregistry API accepts in a
39+ * single batch request.
40+ */
41+ export const DEFAULT_MAX_BATCH_SIZE = 1024
42+
3543/**
3644 * Represents the configuration for the Ipregistry API client.
3745 * This class holds the API key, base URL, and timeout setting used for API requests.
@@ -81,6 +89,19 @@ export class IpregistryConfig {
8189 */
8290 public readonly retryOnTooManyRequests : boolean = false
8391
92+ /**
93+ * The maximum number of values sent in a single batch request. Larger
94+ * batches are split into this many values per request. Capped at
95+ * `DEFAULT_MAX_BATCH_SIZE` (the API limit).
96+ */
97+ public readonly maxBatchSize : number = DEFAULT_MAX_BATCH_SIZE
98+
99+ /**
100+ * How many batch sub-requests are dispatched concurrently when a batch is
101+ * large enough to be split into chunks. Defaults to 4.
102+ */
103+ public readonly batchConcurrency : number = 4
104+
84105 /**
85106 * Constructs a new `IpregistryConfig` instance.
86107 * @param apiKey The API key for authenticating requests.
@@ -90,6 +111,8 @@ export class IpregistryConfig {
90111 * @param retryInterval Optional. The base backoff between retries in milliseconds.
91112 * @param retryOnServerError Optional. Whether 5xx responses are retried.
92113 * @param retryOnTooManyRequests Optional. Whether 429 responses are retried.
114+ * @param maxBatchSize Optional. The maximum number of values per batch request.
115+ * @param batchConcurrency Optional. How many batch sub-requests run concurrently.
93116 */
94117 constructor (
95118 apiKey : string ,
@@ -99,6 +122,8 @@ export class IpregistryConfig {
99122 retryInterval ?: number ,
100123 retryOnServerError ?: boolean ,
101124 retryOnTooManyRequests ?: boolean ,
125+ maxBatchSize ?: number ,
126+ batchConcurrency ?: number ,
102127 ) {
103128 this . apiKey = apiKey
104129
@@ -125,6 +150,18 @@ export class IpregistryConfig {
125150 if ( retryOnTooManyRequests !== undefined ) {
126151 this . retryOnTooManyRequests = retryOnTooManyRequests
127152 }
153+
154+ if (
155+ maxBatchSize !== undefined &&
156+ maxBatchSize > 0 &&
157+ maxBatchSize <= DEFAULT_MAX_BATCH_SIZE
158+ ) {
159+ this . maxBatchSize = maxBatchSize
160+ }
161+
162+ if ( batchConcurrency !== undefined && batchConcurrency > 0 ) {
163+ this . batchConcurrency = batchConcurrency
164+ }
128165 }
129166}
130167
@@ -148,6 +185,10 @@ export class IpregistryConfigBuilder {
148185
149186 private retryOnTooManyRequests : boolean = false
150187
188+ private maxBatchSize : number = DEFAULT_MAX_BATCH_SIZE
189+
190+ private batchConcurrency : number = 4
191+
151192 constructor ( apiKey : string ) {
152193 this . apiKey = apiKey
153194 }
@@ -222,6 +263,34 @@ export class IpregistryConfigBuilder {
222263 return this
223264 }
224265
266+ /**
267+ * Sets the maximum number of values sent in a single batch request. Batch
268+ * lookups split larger inputs into this many values per request. Values
269+ * are capped at `DEFAULT_MAX_BATCH_SIZE` (the API limit); a value <= 0 is
270+ * ignored.
271+ * @param maxBatchSize The maximum number of values per batch request.
272+ * @returns The `IpregistryConfigBuilder` instance for chaining.
273+ */
274+ public withMaxBatchSize ( maxBatchSize : number ) : IpregistryConfigBuilder {
275+ this . maxBatchSize = maxBatchSize
276+ return this
277+ }
278+
279+ /**
280+ * Sets how many batch sub-requests are dispatched concurrently when a
281+ * batch is large enough to be split into chunks. A value <= 0 is ignored.
282+ * Set it to 1 for strictly sequential dispatch, which is gentler on a
283+ * rate-limited API key.
284+ * @param batchConcurrency How many batch sub-requests run concurrently.
285+ * @returns The `IpregistryConfigBuilder` instance for chaining.
286+ */
287+ public withBatchConcurrency (
288+ batchConcurrency : number ,
289+ ) : IpregistryConfigBuilder {
290+ this . batchConcurrency = batchConcurrency
291+ return this
292+ }
293+
225294 public build ( ) : IpregistryConfig {
226295 return new IpregistryConfig (
227296 this . apiKey ,
@@ -231,6 +300,8 @@ export class IpregistryConfigBuilder {
231300 this . retryInterval ,
232301 this . retryOnServerError ,
233302 this . retryOnTooManyRequests ,
303+ this . maxBatchSize ,
304+ this . batchConcurrency ,
234305 )
235306 }
236307}
@@ -312,21 +383,12 @@ export class IpregistryClient {
312383 AutonomousSystem | LookupError
313384 > ( asns . length )
314385
315- let apiResponse : ApiResponse <
316- BatchResult < AutonomousSystem | LookupError >
317- > | null
318- let freshAutonomousSystem : ( AutonomousSystem | LookupError ) [ ]
319-
320- if ( cacheMisses . length > 0 ) {
321- apiResponse = await this . requestHandler . batchLookupAsns (
322- cacheMisses ,
323- options ,
324- )
325- freshAutonomousSystem = apiResponse . data . results
326- } else {
327- apiResponse = null
328- freshAutonomousSystem = [ ]
329- }
386+ const apiResponse = await this . dispatchBatchChunks ( cacheMisses , chunk =>
387+ this . requestHandler . batchLookupAsns ( chunk , options ) ,
388+ )
389+ const freshAutonomousSystem = apiResponse
390+ ? apiResponse . data . results
391+ : [ ]
330392
331393 let j = 0
332394 let k = 0
@@ -403,19 +465,10 @@ export class IpregistryClient {
403465 IpInfo | LookupError
404466 > ( ips . length )
405467
406- let apiResponse : ApiResponse < BatchResult < IpInfo | LookupError > > | null
407- let freshIpInfo : ( IpInfo | LookupError ) [ ]
408-
409- if ( cacheMisses . length > 0 ) {
410- apiResponse = await this . requestHandler . batchLookupIps (
411- cacheMisses ,
412- options ,
413- )
414- freshIpInfo = apiResponse . data . results
415- } else {
416- apiResponse = null
417- freshIpInfo = [ ]
418- }
468+ const apiResponse = await this . dispatchBatchChunks ( cacheMisses , chunk =>
469+ this . requestHandler . batchLookupIps ( chunk , options ) ,
470+ )
471+ const freshIpInfo = apiResponse ? apiResponse . data . results : [ ]
419472
420473 let j = 0
421474 let k = 0
@@ -574,6 +627,113 @@ export class IpregistryClient {
574627 return this . cache
575628 }
576629
630+ /**
631+ * Resolves batch values, splitting inputs larger than `maxBatchSize` into
632+ * chunks dispatched with at most `batchConcurrency` requests in flight,
633+ * and concatenating their results in order. When a chunk fails, the first
634+ * error is thrown and no further chunk is dispatched (in-flight chunks
635+ * complete but their results are discarded). Returns null when there is
636+ * nothing to resolve.
637+ */
638+ private async dispatchBatchChunks < V , R > (
639+ values : V [ ] ,
640+ request : ( chunk : V [ ] ) => Promise < ApiResponse < BatchResult < R > > > ,
641+ ) : Promise < ApiResponse < BatchResult < R > > | null > {
642+ if ( values . length === 0 ) {
643+ return null
644+ }
645+
646+ const { batchConcurrency, maxBatchSize } = this . config
647+
648+ if ( values . length <= maxBatchSize ) {
649+ return await request ( values )
650+ }
651+
652+ const chunks : V [ ] [ ] = [ ]
653+ for ( let start = 0 ; start < values . length ; start += maxBatchSize ) {
654+ chunks . push ( values . slice ( start , start + maxBatchSize ) )
655+ }
656+
657+ const responses : ApiResponse < BatchResult < R > > [ ] = new Array (
658+ chunks . length ,
659+ )
660+ let nextChunk = 0
661+ let firstError : unknown = null
662+
663+ const worker = async ( ) => {
664+ while ( firstError === null ) {
665+ const index = nextChunk ++
666+ if ( index >= chunks . length ) {
667+ return
668+ }
669+ try {
670+ responses [ index ] = await request ( chunks [ index ] )
671+ } catch ( error ) {
672+ firstError = firstError ?? error
673+ return
674+ }
675+ }
676+ }
677+
678+ await Promise . all (
679+ Array . from (
680+ { length : Math . min ( batchConcurrency , chunks . length ) } ,
681+ ( ) => worker ( ) ,
682+ ) ,
683+ )
684+
685+ if ( firstError !== null ) {
686+ throw firstError
687+ }
688+
689+ const results : R [ ] = [ ]
690+ for ( const response of responses ) {
691+ results . push ( ...response . data . results )
692+ }
693+
694+ return {
695+ credits : IpregistryClient . aggregateCredits ( responses ) ,
696+ data : { results } ,
697+ throttling : IpregistryClient . mostConstrainedThrottling ( responses ) ,
698+ }
699+ }
700+
701+ private static aggregateCredits (
702+ responses : ApiResponse < unknown > [ ] ,
703+ ) : ApiResponseCredits {
704+ const consumed = responses
705+ . map ( response => response . credits . consumed )
706+ . filter ( ( value ) : value is number => value !== null )
707+ const remaining = responses
708+ . map ( response => response . credits . remaining )
709+ . filter ( ( value ) : value is number => value !== null )
710+
711+ return {
712+ consumed : consumed . length
713+ ? consumed . reduce ( ( total , value ) => total + value , 0 )
714+ : null ,
715+ remaining : remaining . length ? Math . min ( ...remaining ) : null ,
716+ }
717+ }
718+
719+ private static mostConstrainedThrottling (
720+ responses : ApiResponse < unknown > [ ] ,
721+ ) : ApiResponseThrottling | null {
722+ let result : ApiResponseThrottling | null = null
723+
724+ for ( const response of responses ) {
725+ const throttling = response . throttling
726+ if (
727+ throttling &&
728+ ( ! result || throttling . remaining < result . remaining )
729+ ) {
730+ result = throttling
731+ }
732+ }
733+
734+ return result
735+ }
736+
577737 private static buildCacheKey (
578738 primaryKey : string ,
579739 options : IpregistryOption [ ] ,
0 commit comments