Skip to content

Conversation

vieiralucas
Copy link
Member

@vieiralucas vieiralucas commented Aug 31, 2025

Add PaginatedRequest class with async iteration capabilities to handle paginated email listing. Includes automatic retry logic for rate limits, forward and backward pagination support.


Summary by cubic

Adds async iterator support to emails.list via a new PaginatedRequest, enabling simple forward/backward pagination with automatic rate-limit retries. Aligns with ENG-4362 and preserves await emails.list(...) for first-page fetching.

  • New Features

    • emails.list now returns PaginatedRequest (Promise-like).
    • Iterate all pages with .iterate() or collect with .toArray().
    • Auto-manages after/before cursors for forward and backward pagination.
    • Retries on rate_limit_exceeded using retryAfter.
  • Migration

    • Existing await usage keeps working for the first page.
    • Return type is now PaginatedRequest; update typings where code expected a plain Promise.
    • To stream all results, use the iterator (e.g., call .iterate()).

gabrielmfern and others added 26 commits August 8, 2025 16:21
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bu Kinoshita <[email protected]>
Co-authored-by: Gabriel Miranda <[email protected]>
Co-authored-by: Bu Kinoshita <[email protected]>
Co-authored-by: Gabriel Miranda <[email protected]>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Carolina de Moraes Josephik <[email protected]>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Miranda <[email protected]>
Co-authored-by: Gabriel Miranda <[email protected]>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
@vieiralucas vieiralucas requested a review from a team as a code owner August 31, 2025 21:32
Copy link
Member Author

@vieiralucas vieiralucas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I started implementing this wanting to give the users an API where they would iterate over T.

But we can't do that and return errors as value. So I'm opening the PR as it is for us to discuss on how to solve that problem.

My suggestion is to change the .iterate() to iterate over the pages instead of over the items. So the iterator would yield Response<PaginatedData<T> instead of just T.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 4 files

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

const response = await this.fetchPageWithRetry(currentOptions);
if (response.error) {
// TODO: how do we expose this error to the caller?
console.error('Error fetching page:', response.error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors during subsequent page fetches are logged and iteration silently stops; throw to let callers handle failures rather than silently truncating results.

Prompt for AI agents
Address the following comment on src/common/paginated-request.ts at line 116:

<comment>Errors during subsequent page fetches are logged and iteration silently stops; throw to let callers handle failures rather than silently truncating results.</comment>

<file context>
@@ -0,0 +1,166 @@
+      const response = await this.fetchPageWithRetry(currentOptions);
+      if (response.error) {
+        // TODO: how do we expose this error to the caller?
+        console.error(&#39;Error fetching page:&#39;, response.error);
+        break;
+      }
</file context>

});
} else {
// TODO: how do we expose this error to the caller?
console.error('Error fetching initial page:', initialPage.error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non–rate-limit errors in the initial request are logged and swallowed, preventing callers from handling failures; throw the error to propagate it to consumers of the async iterator.

Prompt for AI agents
Address the following comment on src/common/paginated-request.ts at line 92:

<comment>Non–rate-limit errors in the initial request are logged and swallowed, preventing callers from handling failures; throw the error to propagate it to consumers of the async iterator.</comment>

<file context>
@@ -0,0 +1,166 @@
+        });
+      } else {
+        // TODO: how do we expose this error to the caller?
+        console.error(&#39;Error fetching initial page:&#39;, initialPage.error);
+        return;
+      }
</file context>


if (
response.error?.name === 'rate_limit_exceeded' &&
response.error.retryAfter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truthiness check on retryAfter may skip retries when retryAfter is 0; for rate_limit_exceeded, retryAfter is always defined by type, so this guard is unnecessary and can prevent immediate retries.

Prompt for AI agents
Address the following comment on src/common/paginated-request.ts at line 143:

<comment>Truthiness check on retryAfter may skip retries when retryAfter is 0; for rate_limit_exceeded, retryAfter is always defined by type, so this guard is unnecessary and can prevent immediate retries.</comment>

<file context>
@@ -0,0 +1,166 @@
+
+    if (
+      response.error?.name === &#39;rate_limit_exceeded&#39; &amp;&amp;
+      response.error.retryAfter
+    ) {
+      const retryAfter = response.error.retryAfter * 1000;
</file context>

@vieiralucas
Copy link
Member Author

@gabrielmfern about handling errors:

Unless we want to use exceptions I though of 2 alternatives for the api to expose the error as value:

  1. iterate on page requests
for await (const page of resend.emails.list().iterate()) {
  if (page.error) {
    // handle the error
    break;
  }

  // iterate page items  
  for (const item of page.data) {
  }
}
  1. iterate on items, but yield a Response<T>
for await (const item of resend.email.list().iterate()) {
  if (item.error) {
    // handle the error
    break;
  }
  
  const email = item.data;
}

@emiliosheinz
Copy link
Contributor

@vieiralucas From an outside perspective, I’d love it if you could explain in more detail what problems we’re trying to solve with this change, and why our current implementation of the emails listing API doesn’t address them. If it is just for an easier developer experience, I think that is a totally valid reason.

Also, if we agree on the solution, I’d lean toward iterating on the pages (option 1) rather than on the items. The reason is that, technically, the implementation isn’t fetching one item at a time. It’s fetching new pages as needed. Because of that, showing an error tied to a specific item feels a bit misleading, since the error actually occurs when fetching the entire next page, not the individual item.

Does that make sense?

gabrielmfern and others added 2 commits September 1, 2025 17:29
Add PaginatedRequest class with async iteration capabilities to handle
paginated email listing. Includes automatic retry logic for rate limits,
forward and backward pagination support.
@vieiralucas vieiralucas force-pushed the feature/eng-4362-introduce-asynciterators-to-node-sdk-list-methods branch from e4a9a49 to c2df484 Compare September 3, 2025 16:50
@vieiralucas
Copy link
Member Author

Hi @emiliosheinz, thanks for the comment.

This is basically us wanting to provide better DX, just pushed a new commit that yields the page instead of the item btw, but we're still doing some discussions internally about this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants