@@ -49,8 +49,15 @@ import {
4949 * description: string,
5050 * stake: number
5151 * }> } nodes
52+ * @property {?{next: ?string} } links - Links object containing pagination information
5253 */
5354
55+ /**
56+ * Default page size limit for optimal pagination performance
57+ * @constant {number}
58+ */
59+ const DEFAULT_PAGE_SIZE = 25 ;
60+
5461/**
5562 * Web-compatible query to get a list of Hedera network node addresses from a mirror node.
5663 * Uses fetch API instead of gRPC for web environments.
@@ -65,7 +72,7 @@ export default class AddressBookQueryWeb extends Query {
6572 /**
6673 * @param {object } props
6774 * @param {FileId | string } [props.fileId]
68- * @param {number } [props.limit]
75+ * @param {number } [props.limit] - Page size limit (defaults to 25 for optimal performance)
6976 */
7077 constructor ( props = { } ) {
7178 super ( ) ;
@@ -232,111 +239,135 @@ export default class AddressBookQueryWeb extends Query {
232239 baseUrl = `${ baseUrl } :${ port } ` ;
233240 }
234241
235- const url = new URL ( `${ baseUrl } /api/v1/network/nodes` ) ;
242+ // Initialize aggregated results
243+ this . _addresses = [ ] ;
244+ let nextUrl = null ;
245+ let isLastPage = false ;
236246
247+ // Build initial URL
248+ const initialUrl = new URL ( `${ baseUrl } /api/v1/network/nodes` ) ;
237249 if ( this . _fileId != null ) {
238- url . searchParams . append ( "file.id" , this . _fileId . toString ( ) ) ;
239- }
240- if ( this . _limit != null ) {
241- url . searchParams . append ( "limit" , this . _limit . toString ( ) ) ;
250+ initialUrl . searchParams . append ( "file.id" , this . _fileId . toString ( ) ) ;
242251 }
243252
244- for ( let attempt = 0 ; attempt <= this . _maxAttempts ; attempt ++ ) {
245- try {
246- // eslint-disable-next-line n/no-unsupported-features/node-builtins
247- const response = await fetch ( url . toString ( ) , {
248- method : "GET" ,
249- headers : {
250- Accept : "application/json" ,
251- } ,
252- signal : requestTimeout
253- ? AbortSignal . timeout ( requestTimeout )
254- : undefined ,
255- } ) ;
253+ // Use the specified limit, or default to DEFAULT_PAGE_SIZE for optimal pagination performance
254+ const effectiveLimit =
255+ this . _limit != null ? this . _limit : DEFAULT_PAGE_SIZE ;
256+ initialUrl . searchParams . append ( "limit" , effectiveLimit . toString ( ) ) ;
257+
258+ // Fetch all pages
259+ while ( ! isLastPage ) {
260+ const currentUrl = nextUrl ? new URL ( nextUrl , baseUrl ) : initialUrl ;
261+
262+ for ( let attempt = 0 ; attempt <= this . _maxAttempts ; attempt ++ ) {
263+ try {
264+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
265+ const response = await fetch ( currentUrl . toString ( ) , {
266+ method : "GET" ,
267+ headers : {
268+ Accept : "application/json" ,
269+ } ,
270+ signal : requestTimeout
271+ ? AbortSignal . timeout ( requestTimeout )
272+ : undefined ,
273+ } ) ;
274+
275+ if ( ! response . ok ) {
276+ throw new Error (
277+ `HTTP error! status: ${ response . status } ` ,
278+ ) ;
279+ }
256280
257- if ( ! response . ok ) {
258- throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
259- }
281+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
282+ const data = /** @type {AddressBookQueryWebResponse } */ (
283+ await response . json ( )
284+ ) ;
260285
261- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
262- const data = /** @type {AddressBookQueryWebResponse } */ (
263- await response . json ( )
264- ) ;
265-
266- const nodes = data . nodes || [ ] ;
267-
268- // eslint-disable-next-line ie11/no-loop-func
269- this . _addresses = nodes . map ( ( node ) =>
270- NodeAddress . fromJSON ( {
271- nodeId : node . node_id . toString ( ) ,
272- accountId : node . node_account_id ,
273- addresses : this . _handleAddressesFromGrpcProxyEndpoint (
274- node ,
275- client ,
276- ) ,
277- certHash : node . node_cert_hash ,
278- publicKey : node . public_key ,
279- description : node . description ,
280- stake : node . stake . toString ( ) ,
281- } ) ,
282- ) ;
283-
284- const addressBook = new NodeAddressBook ( {
285- nodeAddresses : this . _addresses ,
286- } ) ;
287-
288- resolve ( addressBook ) ;
289- return ;
290- } catch ( error ) {
291- console . error ( "Error in _makeFetchRequest:" , error ) ;
292- const message =
293- error instanceof Error ? error . message : String ( error ) ;
294-
295- // Check if we should retry
296- if (
297- attempt < this . _maxAttempts &&
298- ! client . isClientShutDown &&
299- this . _retryHandler (
300- /** @type {MirrorError | Error | null } */ ( error ) ,
301- )
302- ) {
303- const delay = Math . min (
304- 250 * 2 ** attempt ,
305- this . _maxBackoff ,
286+ const nodes = data . nodes || [ ] ;
287+
288+ // Aggregate nodes from this page
289+ const pageNodes = nodes . map ( ( node ) =>
290+ NodeAddress . fromJSON ( {
291+ nodeId : node . node_id . toString ( ) ,
292+ accountId : node . node_account_id ,
293+ addresses :
294+ this . _handleAddressesFromGrpcProxyEndpoint (
295+ node ,
296+ client ,
297+ ) ,
298+ certHash : node . node_cert_hash ,
299+ publicKey : node . public_key ,
300+ description : node . description ,
301+ stake : node . stake . toString ( ) ,
302+ } ) ,
306303 ) ;
307304
308- if ( this . _logger ) {
309- this . _logger . debug (
310- `Error getting nodes from mirror for file ${
311- this . _fileId != null
312- ? this . _fileId . toString ( )
313- : "UNKNOWN"
314- } during attempt ${
315- attempt + 1
316- } . Waiting ${ delay } ms before next attempt: ${ message } `,
305+ this . _addresses . push ( ...pageNodes ) ;
306+ nextUrl = data . links ?. next || null ;
307+
308+ // If no more pages, set flag to exit loop
309+ if ( ! nextUrl ) {
310+ isLastPage = true ;
311+ }
312+
313+ // Move to next page
314+ break ;
315+ } catch ( error ) {
316+ console . error ( "Error in _makeFetchRequest:" , error ) ;
317+ const message =
318+ error instanceof Error ? error . message : String ( error ) ;
319+
320+ // Check if we should retry
321+ if (
322+ attempt < this . _maxAttempts &&
323+ ! client . isClientShutDown &&
324+ this . _retryHandler (
325+ /** @type {MirrorError | Error | null } */ ( error ) ,
326+ )
327+ ) {
328+ const delay = Math . min (
329+ 250 * 2 ** attempt ,
330+ this . _maxBackoff ,
317331 ) ;
332+
333+ if ( this . _logger ) {
334+ this . _logger . debug (
335+ `Error getting nodes from mirror for file ${
336+ this . _fileId != null
337+ ? this . _fileId . toString ( )
338+ : "UNKNOWN"
339+ } during attempt ${
340+ attempt + 1
341+ } . Waiting ${ delay } ms before next attempt: ${ message } `,
342+ ) ;
343+ }
344+
345+ // Wait before next attempt
346+ // eslint-disable-next-line ie11/no-loop-func
347+ await new Promise ( ( resolve ) =>
348+ setTimeout ( resolve , delay ) ,
349+ ) ;
350+ continue ;
318351 }
319352
320- // Wait before next attempt
321- // eslint-disable-next-line ie11/no-loop-func
322- await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
323- continue ;
353+ // If we shouldn't retry or have exhausted attempts, reject
354+ const maxAttemptsReached = attempt >= this . _maxAttempts ;
355+ const errorMessage = maxAttemptsReached
356+ ? `Failed to query address book after ${
357+ this . _maxAttempts + 1
358+ } attempts. Last error: ${ message } `
359+ : `Failed to query address book: ${ message } ` ;
360+ reject ( new Error ( errorMessage ) ) ;
361+ return ;
324362 }
325-
326- // If we shouldn't retry or have exhausted attempts, reject
327- const maxAttemptsReached = attempt >= this . _maxAttempts ;
328- const errorMessage = maxAttemptsReached
329- ? `Failed to query address book after ${
330- this . _maxAttempts + 1
331- } attempts. Last error: ${ message } `
332- : `Failed to query address book: ${ message } ` ;
333- reject ( new Error ( errorMessage ) ) ;
334- return ;
335363 }
336364 }
337365
338- // This should never be reached, but just in case
339- reject ( new Error ( "failed to query address book" ) ) ;
366+ // Return the aggregated results
367+ const addressBook = new NodeAddressBook ( {
368+ nodeAddresses : this . _addresses ,
369+ } ) ;
370+ resolve ( addressBook ) ;
340371 }
341372
342373 /**
0 commit comments