Skip to content

Chore/abort controller#629

Draft
ivan-angjelkoski wants to merge 3 commits intodevfrom
chore/abort-controller
Draft

Chore/abort controller#629
ivan-angjelkoski wants to merge 3 commits intodevfrom
chore/abort-controller

Conversation

@ivan-angjelkoski
Copy link
Copy Markdown
Contributor

@ivan-angjelkoski ivan-angjelkoski commented Mar 24, 2026

This pull request adds support for passing an abort signal to gRPC API calls across several modules, enabling consumers to cancel in-flight requests more easily. It introduces an optional GrpcCallOptions parameter (with an AbortSignal) to relevant API methods and propagates this support through the base gRPC consumer logic, including proper handling of abort events and error propagation.

Key changes include:

API Method Enhancements:

  • Added an optional GrpcCallOptions parameter (with signal) to most public gRPC API methods in AbacusGrpcApi, ChainGrpcAuctionApi, ChainGrpcAuthApi, ChainGrpcAuthZApi, and ChainGrpcBankApi, allowing consumers to cancel requests. [1] [2] [3] [4] [5]

Base gRPC Consumer Improvements:

  • Updated BaseGrpcConsumer to accept an optional AbortSignal in its getRpcOptions, retry, and executeGrpcCall methods. Now, if a signal is aborted, the call is cancelled and an appropriate error is thrown. [1] [2] [3]
  • Enhanced error handling in handleGrpcError to treat abort errors as gRPC cancellations, providing a consistent exception for consumers.

Method Signatures and Internal Usage:

  • Modified method signatures and internal calls in all affected APIs to propagate the options?.signal parameter down to the base consumer logic. [1] [2] [3] [4] [5] [6] [7] [8] [9]

These changes make the SDK more robust and responsive, especially in environments where request cancellation is important (such as UI applications or serverless functions).

References:
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12] [13] [14]

Summary by CodeRabbit

  • New Features
    • Added request cancellation support across all gRPC and REST API methods, enabling developers to abort pending requests and implement request timeouts via abort signals.
    • All query and transaction methods now accept optional call configuration for improved request lifecycle control.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

This pull request adds comprehensive abort signal support across the TypeScript SDK, enabling request cancellation for both gRPC and REST API calls. A new CallOptions type with signal?: AbortSignal is introduced, and BaseGrpcConsumer and HTTP clients are enhanced to handle abort signals in retry logic. Optional gRPC/REST options parameters are added to approximately 80+ public API methods across chain, indexer, OLP, and core transaction clients, threading abort signals through the call stack.

Changes

Cohort / File(s) Summary
Type Definitions
packages/sdk-ts/src/types/grpc.ts
Added CallOptions interface with optional signal?: AbortSignal property and GrpcCallOptions type alias for consistent call cancellation across APIs.
Core gRPC Consumer
packages/sdk-ts/src/client/base/BaseGrpcConsumer.ts
Enhanced getRpcOptions, retry, handleGrpcError, and executeGrpcCall to accept and propagate AbortSignal, including abort checks before/during retry attempts and conversion of AbortError to GrpcUnaryRequestException with Canceled error code.
HTTP Client Base
packages/utils/src/classes/HttpClient.ts, packages/utils/src/classes/HttpRestClient.ts
Extended get, post, put, delete methods to accept optional AbortSignal parameter; updated retry helper in HttpRestClient to check abort status and convert axios cancellations into HttpRequestException.
Pagination Utility
packages/sdk-ts/src/utils/pagination.ts
Added signal?: AbortSignal parameter to fetchAllWithPagination, with early abort checks and signal propagation through recursive pagination calls.
Chain gRPC APIs
packages/sdk-ts/src/client/chain/grpc/ChainGrpc*.ts (14 files)
Extended 40+ public methods across AbacusGrpcApi, AuctionApi, AuthApi, AuthZApi, BankApi, DistributionApi, Erc20Api, EvmApi, ExchangeApi, GovApi, IbcApi, InsuranceFundApi, MintApi, OracleApi, PeggyApi, PermissionsApi, StakingApi, TendermintApi, TokenFactoryApi, TxFeesApi, WasmApi, WasmXApi to accept optional GrpcCallOptions and forward signal to executeGrpcCall.
Chain REST APIs
packages/sdk-ts/src/client/chain/rest/ChainRest*.ts (4 files)
Updated AuthApi, BankApi, TendermintApi, WasmApi to accept CallOptions and thread signal through retry and HTTP GET/POST calls.
Indexer gRPC APIs
packages/sdk-ts/src/client/indexer/grpc/IndexerGrpc*.ts (17 files)
Extended 70+ methods across AccountApi, ArchiverApi, AuctionApi, CampaignApi, DerivativesApi, ExplorerApi, InsuranceFundApi, MegaVaultApi, MetaApi, MitoApi, OracleApi, PortfolioApi, ReferralApi, RfqApi, SpotApi, TcDerivativesApi, TradingApi, TransactionApi, Web3GwApi to accept GrpcCallOptions and propagate signal.
Indexer REST APIs
packages/sdk-ts/src/client/indexer/rest/IndexerRest*.ts (5 files)
Updated DerivativesChronosApi, ExplorerApi, LeaderboardChronosApi, MarketChronosApi, SpotChronosApi to accept CallOptions, with signal threaded into this.get() and retry() calls.
OLP & TcAbacus gRPC APIs
packages/sdk-ts/src/client/olp/grpc/OLPGrpcApi.ts, packages/sdk-ts/src/client/tcAbacus/grpc/TcAbacusGrpcApi.ts
Added GrpcCallOptions parameter to 11 methods in OLPGrpcApi and 5 methods in TcAbacusGrpcApi, forwarding signal to executeGrpcCall.
Core Transaction API
packages/sdk-ts/src/core/tx/api/TxRestApi.ts
Extended fetchTx, fetchTxPoll, simulate, broadcast, and broadcastBlock to accept CallOptions; added abort checks and signal propagation through polling loops and private request helpers (broadcastTx, getRaw, postRaw).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as gRPC/REST API
    participant Retry as Retry Handler
    participant HTTP as HTTP Client
    participant Server

    Client->>API: fetchData(params, {signal: abortController.signal})
    API->>Retry: execute with signal
    
    alt Signal Already Aborted
        Retry->>Retry: Check signal.aborted
        Retry-->>API: Throw AbortError
        API-->>Client: Rejected Promise
    else Signal Active
        Retry->>HTTP: Call request with signal
        HTTP->>Server: Send request
        
        alt Request Completes
            Server-->>HTTP: Response
            HTTP-->>Retry: Return data
            Retry-->>API: Return result
            API-->>Client: Resolved Promise
        else Signal Aborted During Request
            Client->>Client: Abort triggered
            Client->>HTTP: Cancel via signal
            HTTP->>Server: Cancel request
            HTTP-->>Retry: Throw AbortError
            Retry-->>API: Throw AbortError
            API-->>Client: Rejected Promise
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop through the code, a signal so clear,
Abort and cancel without any fear!
From gRPC to REST, through retry and call,
Your requests now listen—we've threaded it all!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Chore/abort controller' is vague and generic; it does not clearly convey what the changeset accomplishes or which main components are affected. Revise the title to be more specific and descriptive, such as 'Add abort signal support to gRPC and REST API calls' or 'Support request cancellation via AbortSignal in API methods'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/abort-controller

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

Copy link
Copy Markdown

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sdk-ts/src/client/chain/rest/ChainRestWasmApi.ts (1)

40-44: ⚠️ Potential issue | 🟡 Minor

Incorrect contextModule value.

The error context uses ChainModule.Bank but this is the Wasm API. This will cause misleading error context when exceptions are thrown. Should use ChainModule.Wasm to match the pattern used in other APIs (e.g., ChainGrpcWasmApi uses ChainModule.Wasm).

🐛 Proposed fix
       throw new HttpRequestException(new Error(e as any), {
         code: UnspecifiedErrorCode,
         context: `${this.endpoint}/${endpoint}`,
-        contextModule: ChainModule.Bank,
+        contextModule: ChainModule.Wasm,
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-ts/src/client/chain/rest/ChainRestWasmApi.ts` around lines 40 -
44, The thrown HttpRequestException in ChainRestWasmApi is using the wrong
module context (ChainModule.Bank) which mislabels Wasm errors; update the
exception construction in ChainRestWasmApi so the contextModule is
ChainModule.Wasm (keep the rest: HttpRequestException(new Error(e as any), {
code: UnspecifiedErrorCode, context: `${this.endpoint}/${endpoint}`,
contextModule: ChainModule.Wasm })) to match ChainGrpcWasmApi and other Wasm
handlers.
🧹 Nitpick comments (4)
packages/sdk-ts/src/core/tx/api/TxRestApi.ts (2)

184-227: Consider documenting the dual options parameters.

The broadcast method accepts both options?: TxClientBroadcastOptions (for timeout/txTimeout) and callOptions?: CallOptions (for abort signal). While this maintains backward compatibility, a brief JSDoc comment would help clarify the distinction between these two parameter groups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-ts/src/core/tx/api/TxRestApi.ts` around lines 184 - 227, Add a
concise JSDoc to the broadcast method clarifying the two separate option
objects: explain that options?: TxClientBroadcastOptions controls timeouts
(timeout and txTimeout) and is used to compute the polling timeout, while
callOptions?: CallOptions carries request-level settings such as an AbortSignal
for cancellation passed to broadcastTx and fetchTxPoll; annotate parameters and
mention default behavior
(DEFAULT_BLOCK_TIMEOUT_HEIGHT/DEFAULT_BLOCK_TIME_IN_SECONDS) and which internal
calls use each (broadcast -> broadcastTx uses callOptions.signal, fetchTxPoll
uses computed timeout). Ensure the JSDoc appears immediately above the public
async broadcast(...) signature and references the TxClientBroadcastOptions and
CallOptions types by name.

106-147: Consider adding abort listener during polling delay.

The abort check at Line 114 only triggers at the start of each loop iteration. If the signal is aborted during the setTimeout delay (Line 135), the loop will complete the current sleep before checking the signal again.

For more responsive cancellation, consider using an abort-aware delay:

♻️ Optional: Abort-aware delay for faster cancellation
-      await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL))
+      await new Promise((resolve, reject) => {
+        const timeout = setTimeout(resolve, POLL_INTERVAL)
+        options?.signal?.addEventListener('abort', () => {
+          clearTimeout(timeout)
+          reject(
+            options.signal!.reason ??
+              new DOMException('The operation was aborted.', 'AbortError'),
+          )
+        }, { once: true })
+      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-ts/src/core/tx/api/TxRestApi.ts` around lines 106 - 147, The
polling loop in fetchTxPoll currently checks options?.signal?.aborted only at
the top of each iteration, so the await new Promise(resolve =>
setTimeout(resolve, POLL_INTERVAL)) call can delay abort handling; update the
sleep to be abort-aware by replacing that setTimeout Promise with one that
listens to options.signal (or uses Promise.race with an abort Promise) and
rejects or resolves immediately when the signal is aborted, referencing
fetchTxPoll, POLL_INTERVAL and options.signal to locate where to change the
delay so cancellation is handled promptly during the polling wait.
packages/sdk-ts/src/client/indexer/rest/IndexerRestExplorerApi.ts (1)

54-64: Inconsistent use of endpoint variable.

The endpoint variable is defined on line 54 but the this.get() call on line 60 duplicates the template literal instead of reusing the variable. Other methods in this file (e.g., fetchBlocks, fetchBlocksWithTx) use the endpoint variable consistently.

♻️ Suggested fix
-        () => this.get(`blocks/${blockHashHeight}`, {}, options?.signal),
+        () => this.get(endpoint, {}, options?.signal),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-ts/src/client/indexer/rest/IndexerRestExplorerApi.ts` around
lines 54 - 64, The code defines endpoint = `blocks/${blockHashHeight}` but then
calls this.get with an inlined template literal; update the retry call in
IndexerRestExplorerApi (the method that fetches a single block) to reuse the
endpoint variable instead of duplicating the template literal — i.e., replace
the inlined string in the this.get(...) invocation inside the retry(...) call
with the endpoint identifier so it matches other methods like
fetchBlocks/fetchBlocksWithTx.
packages/sdk-ts/src/client/chain/grpc/ChainGrpcExchangeApi.ts (1)

286-290: Consider object-based parameters for better ergonomics (future improvement).

Methods with multiple optional positional parameters before options (like fetchDerivativeMarkets, fetchSpotMarkets, fetchFullSpotMarkets) require callers to pass undefined for unused parameters when only specifying the abort signal:

api.fetchDerivativeMarkets(undefined, undefined, { signal: controller.signal })

For a future refactor, consider consolidating into an options object pattern. This is not blocking for this PR since it maintains consistency with the existing API structure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-ts/src/client/chain/grpc/ChainGrpcExchangeApi.ts` around lines
286 - 290, Change the multi-optional positional parameters to a single options
object to improve ergonomics: replace the signature of
fetchDerivativeMarkets(status?: string, marketIds?: string[], options?:
GrpcCallOptions) with a single parameter like fetchDerivativeMarkets(opts?: {
status?: string; marketIds?: string[] } & GrpcCallOptions) and update internal
references to read opts.status, opts.marketIds, and opts.signal; apply the same
pattern to fetchSpotMarkets and fetchFullSpotMarkets so callers can call
api.fetchDerivativeMarkets({ marketIds: [...], signal: controller.signal })
without passing undefined for unused args.
🤖 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/sdk-ts/src/client/base/BaseGrpcConsumer.ts`:
- Around line 129-151: The abort listener added to the AbortSignal in
BaseGrpcConsumer.retryGrpcCall is not removed when the timeout fires or when the
retry resolves, which can leak listeners if the same AbortController is reused;
fix by using a named handler (e.g., const onAbort = () => { ... }) passed to
signal.addEventListener and call signal.removeEventListener(onAbort) before
resolving the Promise in the timeout callback (clearTimeout already exists) and
also remove the listener whenever you resolve or reject the Promise from other
code paths so the onAbort handler is always detached.

In `@packages/utils/src/classes/HttpClient.ts`:
- Line 37: The HTTP methods currently spread this.config after method-level
options so config can override per-request cancellation; update each HTTP method
(the calls to this.client.get, this.client.post, this.client.put,
this.client.delete) to spread ...this.config first and then pass method-specific
options (params, signal, body, etc.) so the method-level signal and other
options always take precedence over the instance config.

In `@packages/utils/src/classes/HttpRestClient.ts`:
- Around line 122-144: The retry promise currently adds an 'abort' listener to
signal but never removes it when the timeout fires, causing orphaned listeners;
update retryHttpCall's Promise so the abort listener is a named function (e.g.,
onAbort) and call signal.removeEventListener('abort', onAbort) before resolving
in the timeout callback (and also after rejecting if you keep the existing
inline reject path) and clear the timeout via timeoutId as you already do;
ensure the listener is registered without relying solely on { once: true } so it
can be explicitly removed on successful retry to prevent memory leaks.

---

Outside diff comments:
In `@packages/sdk-ts/src/client/chain/rest/ChainRestWasmApi.ts`:
- Around line 40-44: The thrown HttpRequestException in ChainRestWasmApi is
using the wrong module context (ChainModule.Bank) which mislabels Wasm errors;
update the exception construction in ChainRestWasmApi so the contextModule is
ChainModule.Wasm (keep the rest: HttpRequestException(new Error(e as any), {
code: UnspecifiedErrorCode, context: `${this.endpoint}/${endpoint}`,
contextModule: ChainModule.Wasm })) to match ChainGrpcWasmApi and other Wasm
handlers.

---

Nitpick comments:
In `@packages/sdk-ts/src/client/chain/grpc/ChainGrpcExchangeApi.ts`:
- Around line 286-290: Change the multi-optional positional parameters to a
single options object to improve ergonomics: replace the signature of
fetchDerivativeMarkets(status?: string, marketIds?: string[], options?:
GrpcCallOptions) with a single parameter like fetchDerivativeMarkets(opts?: {
status?: string; marketIds?: string[] } & GrpcCallOptions) and update internal
references to read opts.status, opts.marketIds, and opts.signal; apply the same
pattern to fetchSpotMarkets and fetchFullSpotMarkets so callers can call
api.fetchDerivativeMarkets({ marketIds: [...], signal: controller.signal })
without passing undefined for unused args.

In `@packages/sdk-ts/src/client/indexer/rest/IndexerRestExplorerApi.ts`:
- Around line 54-64: The code defines endpoint = `blocks/${blockHashHeight}` but
then calls this.get with an inlined template literal; update the retry call in
IndexerRestExplorerApi (the method that fetches a single block) to reuse the
endpoint variable instead of duplicating the template literal — i.e., replace
the inlined string in the this.get(...) invocation inside the retry(...) call
with the endpoint identifier so it matches other methods like
fetchBlocks/fetchBlocksWithTx.

In `@packages/sdk-ts/src/core/tx/api/TxRestApi.ts`:
- Around line 184-227: Add a concise JSDoc to the broadcast method clarifying
the two separate option objects: explain that options?: TxClientBroadcastOptions
controls timeouts (timeout and txTimeout) and is used to compute the polling
timeout, while callOptions?: CallOptions carries request-level settings such as
an AbortSignal for cancellation passed to broadcastTx and fetchTxPoll; annotate
parameters and mention default behavior
(DEFAULT_BLOCK_TIMEOUT_HEIGHT/DEFAULT_BLOCK_TIME_IN_SECONDS) and which internal
calls use each (broadcast -> broadcastTx uses callOptions.signal, fetchTxPoll
uses computed timeout). Ensure the JSDoc appears immediately above the public
async broadcast(...) signature and references the TxClientBroadcastOptions and
CallOptions types by name.
- Around line 106-147: The polling loop in fetchTxPoll currently checks
options?.signal?.aborted only at the top of each iteration, so the await new
Promise(resolve => setTimeout(resolve, POLL_INTERVAL)) call can delay abort
handling; update the sleep to be abort-aware by replacing that setTimeout
Promise with one that listens to options.signal (or uses Promise.race with an
abort Promise) and rejects or resolves immediately when the signal is aborted,
referencing fetchTxPoll, POLL_INTERVAL and options.signal to locate where to
change the delay so cancellation is handled promptly during the polling wait.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21f86ede-ba4e-4d57-8aef-acb47801fb17

📥 Commits

Reviewing files that changed from the base of the PR and between b5e0b2c and 982655b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (58)
  • packages/sdk-ts/src/client/abacus/grpc/AbacusGrpcApi.ts
  • packages/sdk-ts/src/client/base/BaseGrpcConsumer.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcAuctionApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcAuthApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcAuthZApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcBankApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcDistributionApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcErc20Api.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcEvmApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcExchangeApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcGovApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcIbcApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcInsuranceFundApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcMintApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcOracleApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcPeggyApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcPermissionsApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcStakingApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcTendermintApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcTokenFactoryApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcTxFeesApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcWasmApi.ts
  • packages/sdk-ts/src/client/chain/grpc/ChainGrpcWasmXApi.ts
  • packages/sdk-ts/src/client/chain/rest/ChainRestAuthApi.ts
  • packages/sdk-ts/src/client/chain/rest/ChainRestBankApi.ts
  • packages/sdk-ts/src/client/chain/rest/ChainRestTendermintApi.ts
  • packages/sdk-ts/src/client/chain/rest/ChainRestWasmApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcAccountApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcArchiverApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcAuctionApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcCampaignApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcDerivativesApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcExplorerApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcInsuranceFundApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcMegaVaultApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcMetaApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcMitoApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcOracleApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcPortfolioApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcReferralApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcRfqApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcSpotApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcTcDerivativesApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcTradingApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcTransactionApi.ts
  • packages/sdk-ts/src/client/indexer/grpc/IndexerGrpcWeb3GwApi.ts
  • packages/sdk-ts/src/client/indexer/rest/IndexerRestDerivativesChronosApi.ts
  • packages/sdk-ts/src/client/indexer/rest/IndexerRestExplorerApi.ts
  • packages/sdk-ts/src/client/indexer/rest/IndexerRestLeaderboardChronosApi.ts
  • packages/sdk-ts/src/client/indexer/rest/IndexerRestMarketChronosApi.ts
  • packages/sdk-ts/src/client/indexer/rest/IndexerRestSpotChronosApi.ts
  • packages/sdk-ts/src/client/olp/grpc/OLPGrpcApi.ts
  • packages/sdk-ts/src/client/tcAbacus/grpc/TcAbacusGrpcApi.ts
  • packages/sdk-ts/src/core/tx/api/TxRestApi.ts
  • packages/sdk-ts/src/types/grpc.ts
  • packages/sdk-ts/src/utils/pagination.ts
  • packages/utils/src/classes/HttpClient.ts
  • packages/utils/src/classes/HttpRestClient.ts

Comment on lines +129 to +151
return new Promise<TResponse>((resolve, reject) => {
const timeoutId = setTimeout(
() => resolve(retryGrpcCall(attempt + 1)),
delay * attempt,
),
)
)

if (signal) {
signal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId)
reject(
signal.reason ??
new DOMException(
'The operation was aborted.',
'AbortError',
),
)
},
{ once: true },
)
}
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential memory leak: abort listener is not removed on successful retry or timeout.

When the retry succeeds or the timeout fires naturally, the abort listener registered on the signal is not cleaned up. If the same AbortController is reused across multiple calls, listeners accumulate.

🛡️ Proposed fix to clean up the abort listener
         return new Promise<TResponse>((resolve, reject) => {
+          const onAbort = () => {
+            clearTimeout(timeoutId)
+            reject(
+              signal.reason ??
+                new DOMException(
+                  'The operation was aborted.',
+                  'AbortError',
+                ),
+            )
+          }
+
           const timeoutId = setTimeout(
-            () => resolve(retryGrpcCall(attempt + 1)),
+            () => {
+              signal?.removeEventListener('abort', onAbort)
+              resolve(retryGrpcCall(attempt + 1))
+            },
             delay * attempt,
           )

           if (signal) {
-            signal.addEventListener(
-              'abort',
-              () => {
-                clearTimeout(timeoutId)
-                reject(
-                  signal.reason ??
-                    new DOMException(
-                      'The operation was aborted.',
-                      'AbortError',
-                    ),
-                )
-              },
-              { once: true },
-            )
+            signal.addEventListener('abort', onAbort, { once: true })
           }
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-ts/src/client/base/BaseGrpcConsumer.ts` around lines 129 - 151,
The abort listener added to the AbortSignal in BaseGrpcConsumer.retryGrpcCall is
not removed when the timeout fires or when the retry resolves, which can leak
listeners if the same AbortController is reused; fix by using a named handler
(e.g., const onAbort = () => { ... }) passed to signal.addEventListener and call
signal.removeEventListener(onAbort) before resolving the Promise in the timeout
callback (clearTimeout already exists) and also remove the listener whenever you
resolve or reject the Promise from other code paths so the onAbort handler is
always detached.

params: T = {} as T,
signal?: AbortSignal,
): Promise<P> {
return this.client.get(endpoint, { params, signal, ...this.config })
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for setConfig calls that may pass signal/params..."
rg -nU --type=ts '\.setConfig\(\s*\{[\s\S]{0,250}?(signal|params)\s*:' packages

echo
echo "Current HttpClient call config ordering:"
sed -n '32,62p' packages/utils/src/classes/HttpClient.ts

Repository: InjectiveLabs/injective-ts

Length of output: 130


🏁 Script executed:

cat -n packages/utils/src/classes/HttpClient.ts | head -70

Repository: InjectiveLabs/injective-ts

Length of output: 1838


🏁 Script executed:

rg -nU --type=ts '\.setConfig\(' packages --max-count=20

Repository: InjectiveLabs/injective-ts

Length of output: 151


🏁 Script executed:

fd -e ts -e js packages | xargs rg 'new HttpClient' | head -20

Repository: InjectiveLabs/injective-ts

Length of output: 333


🏁 Script executed:

cat -n packages/utils/src/classes/HttpRestClient.ts | sed -n '30,45p'

Repository: InjectiveLabs/injective-ts

Length of output: 560


🏁 Script executed:

rg -nU --type=ts 'setConfig' packages -B 3 -A 3

Repository: InjectiveLabs/injective-ts

Length of output: 1033


🏁 Script executed:

rg -nU --type=ts 'setConfig\(' packages -B 2 -A 5 | head -50

Repository: InjectiveLabs/injective-ts

Length of output: 1104


🏁 Script executed:

rg -nU --type=ts 'signal.*:' packages -B 3 -A 1 | grep -A 4 -B 4 'setConfig'

Repository: InjectiveLabs/injective-ts

Length of output: 52


🏁 Script executed:

fd -e ts packages | xargs grep -l 'setConfig' | head -10

Repository: InjectiveLabs/injective-ts

Length of output: 52


🏁 Script executed:

rg -nU --type=ts 'setConfig\(' --context=8 packages

Repository: InjectiveLabs/injective-ts

Length of output: 2353


🏁 Script executed:

rg -nU --type=ts 'setConfig\s*\(' --context=12 packages | grep -v '^--$' | head -80

Repository: InjectiveLabs/injective-ts

Length of output: 3462


🏁 Script executed:

git log --oneline --all -S 'setConfig' -- packages | head -20

Repository: InjectiveLabs/injective-ts

Length of output: 156


🏁 Script executed:

cat -n packages/utils/src/classes/HttpRestClient.ts | sed -n '42,80p'

Repository: InjectiveLabs/injective-ts

Length of output: 1497


🏁 Script executed:

rg -nU --type=ts 'setConfig.*\{' packages -A 3 | head -40

Repository: InjectiveLabs/injective-ts

Length of output: 645


🏁 Script executed:

rg -nU --type=ts 'signal.*AbortSignal' packages | head -20

Repository: InjectiveLabs/injective-ts

Length of output: 1206


🏁 Script executed:

rg -nU --type=ts 'setConfig\(' packages --context=6 | grep -A 10 'setConfig' | head -50

Repository: InjectiveLabs/injective-ts

Length of output: 1198


🏁 Script executed:

python3 << 'EOF'
# Check JavaScript object spread precedence behavior
# Later properties override earlier ones with same key

test_spread = {
    "params": "from_config",
    "signal": "from_config",
    "timeout": 5000
}

# Order 1: { params, signal, ...this.config }
# This means: params (undefined), signal (method param), then config spreads in
# If config has signal, it wins

# Order 2: { ...this.config, params, signal }
# This means: config spreads first, then params and signal from method override

print("Current order problem:")
print("  { params, signal, ...this.config }")
print("  If this.config has 'signal', it overrides the method parameter")
print()
print("Proposed order:")
print("  { ...this.config, params, signal }")
print("  Method parameters always override config properties")
print()
print("This is a valid concern for defensive programming.")
EOF

Repository: InjectiveLabs/injective-ts

Length of output: 354


🏁 Script executed:

cat -n packages/utils/src/classes/HttpClient.ts | sed -n '26,62p'

Repository: InjectiveLabs/injective-ts

Length of output: 1156


Reorder spread to ensure method-level signal takes precedence over config.

In all HTTP methods, ...this.config spreads after signal, allowing setConfig() to override per-request cancellation. This is unsafe for request cancellation which is timing-critical. Move ...this.config first so method parameters always win.

Apply to all four methods
-    return this.client.get(endpoint, { params, signal, ...this.config })
+    return this.client.get(endpoint, { ...this.config, params, signal })
-    return this.client.post(endpoint, data, { signal, ...this.config })
+    return this.client.post(endpoint, data, { ...this.config, signal })
-    return this.client.put(endpoint, data, { signal, ...this.config })
+    return this.client.put(endpoint, data, { ...this.config, signal })
-    return this.client.delete(endpoint, { params, signal, ...this.config })
+    return this.client.delete(endpoint, { ...this.config, params, signal })

Applies to lines 37, 45, 53, 61 in packages/utils/src/classes/HttpClient.ts.

📝 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
return this.client.get(endpoint, { params, signal, ...this.config })
return this.client.get(endpoint, { ...this.config, params, signal })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/utils/src/classes/HttpClient.ts` at line 37, The HTTP methods
currently spread this.config after method-level options so config can override
per-request cancellation; update each HTTP method (the calls to this.client.get,
this.client.post, this.client.put, this.client.delete) to spread ...this.config
first and then pass method-specific options (params, signal, body, etc.) so the
method-level signal and other options always take precedence over the instance
config.

Comment on lines +122 to +144
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(
() => resolve(retryHttpCall(attempt + 1)),
delay * attempt,
),
)
)

if (signal) {
signal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId)
reject(
signal.reason ??
new DOMException(
'The operation was aborted.',
'AbortError',
),
)
},
{ once: true },
)
}
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential memory leak: abort listener not removed on successful retry.

When the timeout fires and resolves the promise, the abort event listener remains attached to the signal. If the signal is long-lived and many retries occur, these orphaned listeners accumulate.

🛡️ Proposed fix to clean up the listener
       return new Promise((resolve, reject) => {
+        const onAbort = () => {
+          clearTimeout(timeoutId)
+          reject(
+            signal.reason ??
+              new DOMException(
+                'The operation was aborted.',
+                'AbortError',
+              ),
+          )
+        }
+
         const timeoutId = setTimeout(
-          () => resolve(retryHttpCall(attempt + 1)),
+          () => {
+            if (signal) {
+              signal.removeEventListener('abort', onAbort)
+            }
+            resolve(retryHttpCall(attempt + 1))
+          },
           delay * attempt,
         )

         if (signal) {
-          signal.addEventListener(
-            'abort',
-            () => {
-              clearTimeout(timeoutId)
-              reject(
-                signal.reason ??
-                  new DOMException(
-                    'The operation was aborted.',
-                    'AbortError',
-                  ),
-              )
-            },
-            { once: true },
-          )
+          signal.addEventListener('abort', onAbort, { once: true })
         }
       })
📝 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
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(
() => resolve(retryHttpCall(attempt + 1)),
delay * attempt,
),
)
)
if (signal) {
signal.addEventListener(
'abort',
() => {
clearTimeout(timeoutId)
reject(
signal.reason ??
new DOMException(
'The operation was aborted.',
'AbortError',
),
)
},
{ once: true },
)
}
})
return new Promise((resolve, reject) => {
const onAbort = () => {
clearTimeout(timeoutId)
reject(
signal.reason ??
new DOMException(
'The operation was aborted.',
'AbortError',
),
)
}
const timeoutId = setTimeout(
() => {
if (signal) {
signal.removeEventListener('abort', onAbort)
}
resolve(retryHttpCall(attempt + 1))
},
delay * attempt,
)
if (signal) {
signal.addEventListener('abort', onAbort, { once: true })
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/utils/src/classes/HttpRestClient.ts` around lines 122 - 144, The
retry promise currently adds an 'abort' listener to signal but never removes it
when the timeout fires, causing orphaned listeners; update retryHttpCall's
Promise so the abort listener is a named function (e.g., onAbort) and call
signal.removeEventListener('abort', onAbort) before resolving in the timeout
callback (and also after rejecting if you keep the existing inline reject path)
and clear the timeout via timeoutId as you already do; ensure the listener is
registered without relying solely on { once: true } so it can be explicitly
removed on successful retry to prevent memory leaks.

@ThomasRalee ThomasRalee marked this pull request as draft March 26, 2026 04:44
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.

1 participant