Skip to content

Resolves #4#20

Merged
AryanSharma48 merged 2 commits into
mainfrom
issue-timeout-support
Jul 3, 2026
Merged

Resolves #4#20
AryanSharma48 merged 2 commits into
mainfrom
issue-timeout-support

Conversation

@AryanSharma48

@AryanSharma48 AryanSharma48 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Description

This PR introduces configurable request timeout support across both the TypeScript and Python packages, allowing users to automatically abort requests that hang indefinitely. It also renames all legacy Resilient configurations to Smooth to align with the new library branding, while keeping the old names as deprecated aliases to maintain backwards compatibility.

Affected packages:

  • smooth-api-ts
  • smooth-api-py

Fixes #4

Type of Change

Please check the option that applies:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update (changes to READMEs, docs, or inline comments)

Checklist

Design & Parity

  • If this introduces a new configuration option or public API, I have implemented equivalent options/behavior in both TypeScript and Python packages.
  • The library remains dependency-free (no new external runtime dependencies added).
  • I have updated the relevant package-specific README.md or general documentation where necessary.

Quality & Testing

  • I started the sandbox Express server (cd sandbox && node server.js) before running the tests.
  • TypeScript Package: I have built and run the TypeScript tests (npm run build && npm test inside packages/smooth-api-ts) and all tests passed.
  • Python Package: I have run the Python tests (pytest tests/ inside packages/smooth-api-py) and all tests passed.
  • I have added new tests to cover my changes.
  • I have commented my code, particularly in hard-to-understand areas, and updated JSDoc/docstrings.

Summary by CodeRabbit

  • New Features

    • Added per-attempt request timeouts for async requests in both Python and TypeScript.
    • Requests can now be aborted after a configured duration, and retries continue as expected when a timeout occurs.
    • Explicit user cancellations now stop immediately without triggering retries.
  • Bug Fixes

    • Improved timeout handling so active requests are cleaned up correctly.
  • Documentation

    • Updated the usage guides to include timeout configuration examples.
  • Chores

    • Bumped package versions to 1.3.0.

@AryanSharma48 AryanSharma48 linked an issue Jul 3, 2026 that may be closed by this pull request
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
smooth-api Ready Ready Preview, Comment Jul 3, 2026 10:05pm

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds configurable request timeout support to both smooth-api-py and smooth-api-ts packages. Python enforces timeouts via asyncio.wait_for (async-only, raising NotImplementedError for sync), while TypeScript uses AbortController. Also renames ResilientFetchConfig/resilientFetch to SmoothFetchConfig/smoothFetch, updates docs, versions, and tests.

Changes

Python package (smooth-api-py)

Layer / File(s) Summary
Timeout config and enforcement
smooth_api/config.py, smooth_api/__init__.py
Adds timeout_ms field to SmoothConfig, wraps async calls with asyncio.wait_for when set, raises NotImplementedError for sync usage, relocates __all__.
Tests, docs, and version
tests/test_timeout.py, README.md, pyproject.toml
Adds async/sync timeout tests, documents the feature and config example, bumps version to 1.3.0.

TypeScript package (smooth-api-ts)

Layer / File(s) Summary
Config type rename and timeoutMs option
src/types.ts, src/dedup.ts
Renames ResilientFetchConfig to SmoothFetchConfig, adds timeoutMs?: number, updates JSDoc references.
AbortController timeout and abort handling
src/index.ts
Implements per-attempt timeout via AbortController, propagates user aborts, re-throws on explicit abort, clears timeouts in finally.
Identifier rename across tests
tests/deduplication.test.ts, tests/resilience.test.ts
Renames resilientFetch variables/calls to smoothFetch throughout existing test suites.
New timeout tests, docs, and version
tests/timeout.test.ts, README.md, package.json
Adds tests for timeout-triggered retry and abort cancellation, documents the feature, bumps version to 1.3.0.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant createSmoothFetch
  participant AbortController
  participant fetch as "global.fetch"
  Caller->>createSmoothFetch: request(url, options)
  createSmoothFetch->>AbortController: create controller
  createSmoothFetch->>AbortController: setTimeout(timeoutMs) to abort
  createSmoothFetch->>fetch: fetch(url, options with signal)
  alt Timeout elapses
    AbortController-->>fetch: abort()
    fetch-->>createSmoothFetch: throws error
    createSmoothFetch->>createSmoothFetch: check user signal.aborted
    createSmoothFetch->>fetch: retry attempt
    fetch-->>createSmoothFetch: response 200
  else User aborts explicitly
    Caller->>AbortController: abort()
    fetch-->>createSmoothFetch: throws error
    createSmoothFetch-->>Caller: re-throw immediately (no retry)
  else Success
    fetch-->>createSmoothFetch: response
    createSmoothFetch-->>Caller: response
  end
  createSmoothFetch->>createSmoothFetch: clear timeout (finally)
Loading

Estimated code review effort

Estimated code review effort: 3 (Moderate) | ~25 minutes

Related issues: #4 (Add Request Timeout Support)

Suggested labels: enhancement, documentation, tests

Suggested reviewers: AryanSharma48

Poem:

A rabbit sets a timer's chime, 🐰⏱️
Async waits, then aborts in time,
Sync ones balk with a raised alarm,
smoothFetch replaces the old fetch's charm,
Docs and tests hop along in rhyme.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The Resilient-to-Smooth rename and version bump go beyond the timeout support requested in #4. Split branding renames and version bumps into a separate PR, or document them as part of the issue scope.
Title check ❓ Inconclusive The title is only the issue reference and does not describe the actual change. Use a descriptive title such as 'Add request timeout support for smooth-api'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR adds configurable request timeouts, aborts overdue requests, updates docs, and adds tests while preserving the no-timeout path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-timeout-support

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

@AryanSharma48
AryanSharma48 merged commit 4a9feb4 into main Jul 3, 2026
6 of 7 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/smooth-api-py/smooth_api/__init__.py (1)

206-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Sort __all__ per Ruff (RUF022).

Static analysis flags the export list as unsorted.

♻️ Proposed fix
-__all__ = ['smooth_api', 'SmoothConfig', 'DeduplicationConfig', 'resilient_api', 'ResilientConfig']
+__all__ = ['DeduplicationConfig', 'ResilientConfig', 'SmoothConfig', 'resilient_api', 'smooth_api']
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/smooth-api-py/smooth_api/__init__.py` around lines 206 - 207, The
__all__ export list in smooth_api.__init__ is unsorted and should be reordered
to satisfy Ruff RUF022. Update the __all__ assignment so the exported symbols
are listed in sorted order, keeping the same names and using the existing
__all__ definition as the location to fix.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/smooth-api-py/smooth_api/__init__.py`:
- Around line 89-92: The timeout handling in the async wrapper currently treats
timeout_ms=0 as falsy, so an explicit zero timeout is skipped instead of
enforced. Update the conditional in the decorator logic around the
fn/asyncio.wait_for call to check config.timeout_ms with an explicit is not None
test, so both 0 and positive values use wait_for while None keeps the no-timeout
path.

In `@packages/smooth-api-ts/src/index.ts`:
- Around line 50-52: The retry path in `index.ts` leaves a bridged abort
listener attached to the caller’s `AbortSignal`, and the per-attempt controller
can also remain referenced after completion. Update the retry logic around the
`options.signal`/`userSignal.addEventListener('abort', ...)` bridge to store the
listener and remove it in the same `finally` block that clears the timeout,
ensuring each attempt cleans up both the abort listener and the controller
reference. Apply the same cleanup to the other retry block referenced by the
duplicate abort-listener setup.
- Around line 109-112: User cancellations are being counted as failures in the
circuit-breaker flow. In the request handling path inside the relevant logic in
index.ts, check the abort state from options?.signal before calling the failure
accounting path, and short-circuit aborted requests without incrementing failure
metrics. Update the surrounding error handling so the abort check happens before
the circuit-breaker failure record, keeping canceled calls from opening the
circuit.

In `@packages/smooth-api-ts/src/types.ts`:
- Line 41: The exported config type was renamed to SmoothFetchConfig, but the
deprecated ResilientFetchConfig alias is no longer exported, which breaks
existing TypeScript consumers. Update the types module to keep
ResilientFetchConfig exported as a deprecated alias alongside SmoothFetchConfig,
using the same underlying shape so both names remain available for imports.
- Around line 53-57: The timeoutMs field in the request types is currently too
permissive and can accept invalid values like 0 or NaN, which then bypass the
intended abort behavior. Update the type definition in types.ts for timeoutMs to
constrain it to a positive finite number, and ensure any validation or usage
around the request timeout path enforces that constraint before scheduling the
abort timer. Use the timeoutMs field and its related request-attempt timeout
handling as the key symbols to locate the fix.

---

Nitpick comments:
In `@packages/smooth-api-py/smooth_api/__init__.py`:
- Around line 206-207: The __all__ export list in smooth_api.__init__ is
unsorted and should be reordered to satisfy Ruff RUF022. Update the __all__
assignment so the exported symbols are listed in sorted order, keeping the same
names and using the existing __all__ definition as the location to fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d212b44a-0664-4c69-b6da-218bc0128fcd

📥 Commits

Reviewing files that changed from the base of the PR and between c60e016 and 79c878d.

📒 Files selected for processing (13)
  • packages/smooth-api-py/README.md
  • packages/smooth-api-py/pyproject.toml
  • packages/smooth-api-py/smooth_api/__init__.py
  • packages/smooth-api-py/smooth_api/config.py
  • packages/smooth-api-py/tests/test_timeout.py
  • packages/smooth-api-ts/README.md
  • packages/smooth-api-ts/package.json
  • packages/smooth-api-ts/src/dedup.ts
  • packages/smooth-api-ts/src/index.ts
  • packages/smooth-api-ts/src/types.ts
  • packages/smooth-api-ts/tests/deduplication.test.ts
  • packages/smooth-api-ts/tests/resilience.test.ts
  • packages/smooth-api-ts/tests/timeout.test.ts

Comment on lines +89 to +92
if config.timeout_ms:
result = await asyncio.wait_for(fn(*args, **kwargs), timeout=config.timeout_ms / 1000.0)
else:
result = await fn(*args, **kwargs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Explicit timeout_ms=0 is silently ignored.

if config.timeout_ms: treats 0 as falsy, so a caller who intentionally sets timeout_ms=0 gets no timeout enforcement at all, silently diverging from the configured value. Since the field is Optional[int], use an explicit is not None check.

🐛 Proposed fix
-                            if config.timeout_ms:
+                            if config.timeout_ms is not None:
                                 result = await asyncio.wait_for(fn(*args, **kwargs), timeout=config.timeout_ms / 1000.0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if config.timeout_ms:
result = await asyncio.wait_for(fn(*args, **kwargs), timeout=config.timeout_ms / 1000.0)
else:
result = await fn(*args, **kwargs)
if config.timeout_ms is not None:
result = await asyncio.wait_for(fn(*args, **kwargs), timeout=config.timeout_ms / 1000.0)
else:
result = await fn(*args, **kwargs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/smooth-api-py/smooth_api/__init__.py` around lines 89 - 92, The
timeout handling in the async wrapper currently treats timeout_ms=0 as falsy, so
an explicit zero timeout is skipped instead of enforced. Update the conditional
in the decorator logic around the fn/asyncio.wait_for call to check
config.timeout_ms with an explicit is not None test, so both 0 and positive
values use wait_for while None keeps the no-timeout path.

Comment on lines +50 to +52
if (options?.signal) {
const userSignal = options.signal;
userSignal.addEventListener('abort', () => controller?.abort(userSignal.reason));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove the bridged abort listener in finally.

Each retry adds a listener to the caller’s AbortSignal and retains the attempt controller after completion. Clean it up with the timeout.

Abort listener cleanup
           let timeoutId: ReturnType<typeof setTimeout> | undefined;
           let currentOptions = options;
           let controller: AbortController | undefined;
+          let removeUserAbortListener: (() => void) | undefined;
 
           if (globalConfig.timeoutMs) {
             controller = new AbortController();
             if (options?.signal) {
               const userSignal = options.signal;
-              userSignal.addEventListener('abort', () => controller?.abort(userSignal.reason));
               if (userSignal.aborted) {
                 controller.abort(userSignal.reason);
+              } else {
+                const abortFromUser = () => controller?.abort(userSignal.reason);
+                userSignal.addEventListener('abort', abortFromUser, { once: true });
+                removeUserAbortListener = () => userSignal.removeEventListener('abort', abortFromUser);
               }
             }
@@
           } finally {
             if (timeoutId) clearTimeout(timeoutId);
+            removeUserAbortListener?.();
           }

Also applies to: 118-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/smooth-api-ts/src/index.ts` around lines 50 - 52, The retry path in
`index.ts` leaves a bridged abort listener attached to the caller’s
`AbortSignal`, and the per-attempt controller can also remain referenced after
completion. Update the retry logic around the
`options.signal`/`userSignal.addEventListener('abort', ...)` bridge to store the
listener and remove it in the same `finally` block that clears the timeout,
ensuring each attempt cleans up both the abort listener and the controller
reference. Apply the same cleanup to the other retry block referenced by the
duplicate abort-listener setup.

Comment on lines +109 to +112
// Do not retry if the user explicitly aborted the request
if (options?.signal?.aborted) {
throw err;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not count user cancellations as circuit-breaker failures.

Line 107 records a failure before Line 110 detects a caller abort, so repeated user cancellations can open the circuit and block later valid requests.

Move abort handling before failure accounting
           } catch (err) {
             lastError = err;
-            breaker.recordFailure(domain);
 
             // Do not retry if the user explicitly aborted the request
             if (options?.signal?.aborted) {
               throw err;
             }
+
+            breaker.recordFailure(domain);
 
             // Don't sleep after the final attempt
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Do not retry if the user explicitly aborted the request
if (options?.signal?.aborted) {
throw err;
}
// Do not retry if the user explicitly aborted the request
if (options?.signal?.aborted) {
throw err;
}
breaker.recordFailure(domain);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/smooth-api-ts/src/index.ts` around lines 109 - 112, User
cancellations are being counted as failures in the circuit-breaker flow. In the
request handling path inside the relevant logic in index.ts, check the abort
state from options?.signal before calling the failure accounting path, and
short-circuit aborted requests without incrementing failure metrics. Update the
surrounding error handling so the abort check happens before the circuit-breaker
failure record, keeping canceled calls from opening the circuit.


// T types the fallback payload so callers get inference at the use site.
export interface ResilientFetchConfig<T = unknown> {
export interface SmoothFetchConfig<T = unknown> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the deprecated alias is still exported from the TS package.
rg -nP '\b(ResilientFetchConfig|SmoothFetchConfig)\b' packages/smooth-api-ts/src packages/smooth-api-ts/tests packages/smooth-api-ts/README.md

Repository: AryanSharma48/smoothAPI

Length of output: 468


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for ResilientFetchConfig across repo ==\n'
rg -nP '\bResilientFetchConfig\b' .

printf '\n== package files ==\n'
git ls-files 'packages/smooth-api-ts/*'

printf '\n== types.ts (relevant slice) ==\n'
sed -n '1,120p' packages/smooth-api-ts/src/types.ts

printf '\n== index.ts (relevant slice) ==\n'
sed -n '1,120p' packages/smooth-api-ts/src/index.ts

printf '\n== package.json ==\n'
sed -n '1,220p' packages/smooth-api-ts/package.json

Repository: AryanSharma48/smoothAPI

Length of output: 212


Keep the deprecated ResilientFetchConfig alias exported. packages/smooth-api-ts/src/types.ts only exposes SmoothFetchConfig, so existing TypeScript consumers importing ResilientFetchConfig will break; add the deprecated alias alongside the new name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/smooth-api-ts/src/types.ts` at line 41, The exported config type was
renamed to SmoothFetchConfig, but the deprecated ResilientFetchConfig alias is
no longer exported, which breaks existing TypeScript consumers. Update the types
module to keep ResilientFetchConfig exported as a deprecated alias alongside
SmoothFetchConfig, using the same underlying shape so both names remain
available for imports.

Comment on lines +53 to +57
/**
* Maximum duration in milliseconds before a request attempt is aborted.
* Applied per-attempt. If aborted, the request is considered a failure and may be retried.
*/
timeoutMs?: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether timeoutMs is validated before use.
rg -nP 'timeoutMs|Number\.isFinite|RangeError' packages/smooth-api-ts/src

Repository: AryanSharma48/smoothAPI

Length of output: 455


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- packages/smooth-api-ts/src/index.ts ---\n'
wc -l packages/smooth-api-ts/src/index.ts packages/smooth-api-ts/src/types.ts

printf '\n--- outline: packages/smooth-api-ts/src/index.ts ---\n'
ast-grep outline packages/smooth-api-ts/src/index.ts --view expanded || true

printf '\n--- relevant excerpt: packages/smooth-api-ts/src/index.ts ---\n'
sed -n '1,120p' packages/smooth-api-ts/src/index.ts

printf '\n--- relevant excerpt: packages/smooth-api-ts/src/types.ts ---\n'
sed -n '1,120p' packages/smooth-api-ts/src/types.ts

Repository: AryanSharma48/smoothAPI

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- packages/smooth-api-ts/src/index.ts line count ---'
wc -l packages/smooth-api-ts/src/index.ts

echo
echo '--- packages/smooth-api-ts/src/types.ts line count ---'
wc -l packages/smooth-api-ts/src/types.ts

echo
echo '--- outline: packages/smooth-api-ts/src/index.ts ---'
ast-grep outline packages/smooth-api-ts/src/index.ts --view expanded || true

echo
echo '--- relevant excerpt: packages/smooth-api-ts/src/index.ts ---'
sed -n '1,120p' packages/smooth-api-ts/src/index.ts

echo
echo '--- relevant excerpt: packages/smooth-api-ts/src/types.ts ---'
sed -n '1,120p' packages/smooth-api-ts/src/types.ts

Repository: AryanSharma48/smoothAPI

Length of output: 7814


Constrain timeoutMs to a positive finite number. timeoutMs is only truthy-checked before scheduling the abort timer, so 0/NaN disable it and other invalid values still flow to setTimeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/smooth-api-ts/src/types.ts` around lines 53 - 57, The timeoutMs
field in the request types is currently too permissive and can accept invalid
values like 0 or NaN, which then bypass the intended abort behavior. Update the
type definition in types.ts for timeoutMs to constrain it to a positive finite
number, and ensure any validation or usage around the request timeout path
enforces that constraint before scheduling the abort timer. Use the timeoutMs
field and its related request-attempt timeout handling as the key symbols to
locate the fix.

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.

Add Request Timeout Support

1 participant