Skip to content

feat(ensnode-sdk) waiting helpers#1709

Closed
tk-o wants to merge 2 commits intomainfrom
feat/ensnode-sdk-waiting-helpers
Closed

feat(ensnode-sdk) waiting helpers#1709
tk-o wants to merge 2 commits intomainfrom
feat/ensnode-sdk-waiting-helpers

Conversation

@tk-o
Copy link
Contributor

@tk-o tk-o commented Mar 3, 2026

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • Added waiting helpers that allow halting runtime until certain event happens.
    • wait() that allows waiting a certain amount of time
    • waitForCondition() that allows waiting until a certain condition turns ture

Why

  • ENSDb Writer Worker has a dependency on ENSIndexer API to be available. With the waiting helpers, it's going to be easy to model as:
await waitForCondition(() =>
  ensIndexerClient.config().then(() => true).catch(() => false)
);

Testing

  • Static code checks (lint, typecheck) + extended testing suite were all OK.

Notes for Reviewer (Optional)

  • Anything non-obvious or worth a heads-up.

Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

@tk-o tk-o requested a review from a team as a code owner March 3, 2026 07:10
Copilot AI review requested due to automatic review settings March 3, 2026 07:10
@changeset-bot
Copy link

changeset-bot bot commented Mar 3, 2026

🦋 Changeset detected

Latest commit: b1c90d3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@ensnode/ensnode-sdk Major
ensadmin Major
ensapi Major
ensindexer Major
ensrainbow Major
fallback-ensapi Major
@namehash/ens-referrals Major
@ensnode/ensnode-react Major
@ensnode/ensrainbow-sdk Major
@namehash/namehash-ui Major
@ensnode/datasources Major
@ensnode/ensnode-schema Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Mar 3, 2026 7:10am
ensnode.io Ready Ready Preview, Comment Mar 3, 2026 7:10am
ensrainbow.io Ready Ready Preview, Comment Mar 3, 2026 7:10am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Introduces a new wait utility module to the SDK with two helper functions: wait() for delaying execution and waitForCondition() for polling an async condition until timeout. Includes comprehensive unit tests and exports the module through the SDK's public API.

Changes

Cohort / File(s) Summary
Wait Utility Module
packages/ensnode-sdk/src/shared/wait.ts, packages/ensnode-sdk/src/index.ts
Adds wait() for delays and waitForCondition() for polling conditions at configurable intervals with timeout. Exports both functions via index for public API access.
Wait Tests
packages/ensnode-sdk/src/shared/wait.test.ts
Comprehensive unit tests covering immediate/delayed resolution, condition polling with retries, timeout errors, error handling during retries, and custom interval verification.
Changeset
.changeset/slick-files-rush.md
Marks minor version bump for SDK with description of new waiting helper functionality.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Hop, wait, and listen with care,
For helpers that pause without a despair,
Poll conditions with patience so kind,
Till the moment you seek, we shall find!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(ensnode-sdk) waiting helpers' clearly summarizes the main change: adding waiting helper utilities to the ensnode SDK.
Description check ✅ Passed The PR description follows the template structure with all required sections completed: Summary, Why, Testing, and Pre-Review Checklist, with relevant details provided for each.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ensnode-sdk-waiting-helpers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR adds two utility helpers — wait(ms) and waitForCondition(conditionFn, options) — to the @ensnode/ensnode-sdk package and exports them from the package's public API. The helpers are intended to support startup orchestration patterns (e.g., polling until a downstream service is healthy) and are a building block for the ENSDb Writer Worker.

Key points:

  • wait(ms) is a straightforward setTimeout-backed promise.
  • waitForCondition polls a condition function at a configurable interval (default 1 s) with a configurable timeout (default 30 s), swallowing errors from the condition and retrying until timeout.
  • date-fns is an existing dependency in the package so using secondsToMilliseconds does not introduce a new dependency.
  • The test suite is thorough, covering the happy path, multi-retry, error swallowing, default options, and timeout scenarios.
  • One logic concern exists: the timeout check is performed after conditionFn() is called on each iteration, so after wait(intervalMs) causes the elapsed time to cross timeoutMs, the condition function is still invoked once more before the timeout is detected. For condition functions that perform slow I/O (e.g., HTTP health checks), the actual elapsed time can exceed timeoutMs by up to intervalMs + conditionFn execution time.

Confidence Score: 3/5

  • Safe to merge after resolving the timeout-boundary logic issue, which can cause the effective wait time to silently overshoot timeoutMs for slow condition functions.
  • The implementation is small and well-tested, but contains a logic issue where conditionFn is invoked one extra time after the timeout has already elapsed. For the primary intended use case (network health checks with potentially long response times), this can cause waitForCondition to block noticeably longer than the configured timeoutMs.
  • packages/ensnode-sdk/src/shared/wait.ts — the timeout-check ordering in the while loop.

Important Files Changed

Filename Overview
packages/ensnode-sdk/src/shared/wait.ts Implements wait and waitForCondition helpers. The timeout check is placed after conditionFn() is called, so after the final wait(intervalMs) crosses the timeout boundary the condition function is invoked one additional time before the timeout error is thrown — potentially causing the actual wait to exceed timeoutMs by intervalMs + conditionFn execution time.
packages/ensnode-sdk/src/shared/wait.test.ts Comprehensive test suite for both helpers using Vitest fake timers; covers success, retry, timeout, error-swallowing, defaults, and custom interval scenarios. Well-structured and readable.
packages/ensnode-sdk/src/index.ts Adds a single re-export line for the new wait module — straightforward and correct.
.changeset/slick-files-rush.md Correctly marks the change as a minor bump for @ensnode/ensnode-sdk with a brief description.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant waitForCondition
    participant conditionFn
    participant wait

    Caller->>waitForCondition: waitForCondition(conditionFn, { intervalMs, timeoutMs })
    loop Until condition met or timeout
        waitForCondition->>conditionFn: conditionFn()
        alt condition returns true
            conditionFn-->>waitForCondition: true
            waitForCondition-->>Caller: resolves (void)
        else condition returns false or throws
            conditionFn-->>waitForCondition: false / Error (swallowed)
            waitForCondition->>waitForCondition: check Date.now() - startTime >= timeoutMs
            alt timed out
                waitForCondition-->>Caller: rejects (Error: "Timeout while waiting for condition")
            else not timed out
                waitForCondition->>wait: wait(intervalMs)
                wait-->>waitForCondition: resolves after intervalMs
            end
        end
    end
Loading

Last reviewed commit: b1c90d3

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +51 to +66
while (true) {
try {
if (await conditionFn()) {
// Condition is met, resolve the promise and exit the loop
return;
}
} catch {
// Ignore errors from the condition function and continue retrying until timeout
}

if (Date.now() - startTime >= timeoutMs) {
throw new Error("Timeout while waiting for condition");
}

await wait(intervalMs);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Extra conditionFn call after timeout boundary is crossed

After each wait(intervalMs) completes, the loop immediately calls conditionFn() again before checking the timeout. This means that once wait() returns at a moment where Date.now() - startTime >= timeoutMs, a full execution of conditionFn() still happens before the timeout is detected.

For the intended usage (e.g., ensIndexerClient.config() which is an HTTP request that may take several seconds to time out or return), this can cause the total elapsed time to significantly overshoot timeoutMs — by up to intervalMs + conditionFn_execution_time.

A simple fix is to check the deadline at the top of the loop, before calling conditionFn, while still always running the condition at least once on the very first iteration:

  const startTime = Date.now();

  while (true) {
    // Check timeout at the start of each iteration (except the very first)
    // so we don't invoke conditionFn after the deadline has already elapsed.
    if (Date.now() - startTime >= timeoutMs) {
      throw new Error("Timeout while waiting for condition");
    }

    try {
      if (await conditionFn()) {
        return;
      }
    } catch {
      // Ignore errors from the condition function and continue retrying until timeout
    }

    await wait(intervalMs);
  }

Note: this slight change means that a timeoutMs: 0 would reject immediately without ever calling conditionFn. If "at least one check before timing out" is a hard requirement, a do/while or a separate first-call-then-loop pattern would preserve that while still preventing post-deadline conditionFn invocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ensnode-sdk/src/shared/wait.ts`:
- Around line 3-11: The JSDoc for the wait function (`wait(ms: number):
Promise<void>`) contains a redundant `@returns` tag; remove the `@returns` line
from the comment block so only the summary and the `@param ms` remain, leaving
the function signature and implementation (`export async function wait`)
unchanged and keeping the doc concise and valid.
- Around line 32-43: Remove the redundant `@returns` JSDoc tag from the JSDoc
block for the waitForCondition function: keep the summary describing the
behavior and retain other useful tags (e.g., `@param`) but delete the duplicate
`@returns` line so the documentation isn't repetitive for the exported async
function waitForCondition.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0de5e9 and b1c90d3.

📒 Files selected for processing (4)
  • .changeset/slick-files-rush.md
  • packages/ensnode-sdk/src/index.ts
  • packages/ensnode-sdk/src/shared/wait.test.ts
  • packages/ensnode-sdk/src/shared/wait.ts

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds generic waiting utilities to @ensnode/ensnode-sdk to support workflows that need to pause execution until a dependency becomes available (e.g., waiting for an API to come up), and exposes them as part of the public SDK surface.

Changes:

  • Introduces wait(ms) and waitForCondition(conditionFn, options) helpers in src/shared/wait.ts.
  • Adds a Vitest suite covering expected wait / waitForCondition behavior.
  • Exports the new helpers from the package root and adds a changeset for a minor bump.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
packages/ensnode-sdk/src/shared/wait.ts New public waiting helpers (wait, waitForCondition) with defaults.
packages/ensnode-sdk/src/shared/wait.test.ts Unit tests for timing, retry, error swallowing, and timeout cases.
packages/ensnode-sdk/src/index.ts Re-exports the new shared module from the SDK entrypoint.
.changeset/slick-files-rush.md Announces the new helpers in release notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +47
const {
intervalMs = DEFAULT_WAIT_FOR_CONDITION_OPTIONS.intervalMs,
timeoutMs = DEFAULT_WAIT_FOR_CONDITION_OPTIONS.timeoutMs,
} = options || {};
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

waitForCondition should validate intervalMs/timeoutMs as finite numbers (and typically intervalMs > 0, timeoutMs >= 0). As written, passing NaN (or a non-finite) timeoutMs makes the timeout check always false, resulting in an infinite loop; intervalMs <= 0 (or NaN, which becomes 0 in setTimeout) can create a tight retry loop.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +59
try {
if (await conditionFn()) {
// Condition is met, resolve the promise and exit the loop
return;
}
} catch {
// Ignore errors from the condition function and continue retrying until timeout
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The catch {} unconditionally swallows all errors from conditionFn. Since this is exported as a public helper, it becomes impossible for callers to fail fast on non-transient errors (e.g. misconfiguration) or propagate cancellation errors; consider either rethrowing by default, or adding an option (e.g. ignoreErrors / shouldRetryOnError(error)) and documenting the behavior explicitly.

Copilot uses AI. Check for mistakes.
}

if (Date.now() - startTime >= timeoutMs) {
throw new Error("Timeout while waiting for condition");
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The timeout error message is quite generic. Since this is likely to surface in operational logs, consider including at least the timeoutMs value (and possibly the last observed error from conditionFn, if you add error handling options) to make debugging easier.

Suggested change
throw new Error("Timeout while waiting for condition");
throw new Error(`Timeout while waiting for condition after ${timeoutMs} ms`);

Copilot uses AI. Check for mistakes.

describe("wait", () => {
beforeEach(() => {
vi.useFakeTimers();
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

This test suite asserts on Date.now() deltas later, but uses vi.useFakeTimers() without pinning now like other timer-based tests in this package (e.g. vi.useFakeTimers({ shouldAdvanceTime: true, now: ... })). Pinning now improves determinism across environments and makes the Date.now()-based assertions less brittle.

Suggested change
vi.useFakeTimers();
vi.useFakeTimers({ now: new Date("2020-01-01T00:00:00Z") });

Copilot uses AI. Check for mistakes.
"@ensnode/ensnode-sdk": minor
---

Added waiting helpers that allow halting runtime until certain event happens.
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

Grammar in the changeset message: "until certain event happens" is missing an article. Consider changing it to "until a certain event happens" (or similar) since this text may appear in release notes.

Suggested change
Added waiting helpers that allow halting runtime until certain event happens.
Added waiting helpers that allow halting runtime until a certain event happens.

Copilot uses AI. Check for mistakes.
@tk-o
Copy link
Contributor Author

tk-o commented Mar 3, 2026

Closing this PR as after reading feedback it feels like re-implementing a well known package:
https://www.npmjs.com/package/p-retry

@tk-o tk-o closed this Mar 3, 2026
tk-o added a commit that referenced this pull request Mar 3, 2026
Initially, I wanted to implement some helpers myself, but then, after seeing AI PR feedback, I figured I was rebuilding `p-retry` in a way... Here is the (now closed) PR: #1709
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants