Skip to content

Commit dd6ced7

Browse files
fix: fetch all nodes from addressbook (#3421)
Signed-off-by: venilinvasilev <[email protected]>
1 parent e849bbf commit dd6ced7

File tree

2 files changed

+408
-91
lines changed

2 files changed

+408
-91
lines changed

src/network/AddressBookQueryWeb.js

Lines changed: 122 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)