refactor(exchange): declare retry policy per endpoint with a @retry decorator - #1200
refactor(exchange): declare retry policy per endpoint with a @retry decorator#1200bennycode wants to merge 4 commits into
Conversation
|
|
||
| type AsyncMethod<This, Args extends unknown[], Result> = (this: This, ...args: Args) => Promise<Result>; | ||
|
|
||
| export interface RetryOptions { |
There was a problem hiding this comment.
extract RetryOptions from axios-retry
| target: 'es2022', | ||
| }, | ||
| plugins: [ |
There was a problem hiding this comment.
this is weird, find another alternative
| } | ||
|
|
||
| /** @see https://t212public-api-docs.redoc.ly/#operation/positionByTicker */ | ||
| @retry() |
There was a problem hiding this comment.
make delay mandatory so we can see what's the retry delay directly at the function
…ecorator Replace the axios-retry interceptor in Trading212API with a standard-mode (TC39) method decorator. The interceptor had to reverse-map failed request URLs back to per-endpoint delays via string matching — a design that already produced one silent fallback bug on paginated URLs. Each API method now declares its own rate-limit delay where the endpoint lives. - Add retry() method decorator with configurable delay, retry limit, and retryable-error predicate (defaults match axiosRetry.isRetryableError: network errors, 429, 5xx — as SimplifiedHttpError statuses 0/429/5xx) - getHistoryOrders now pages through the decorated getHistoryOrdersPage, preserving per-page retry and removing duplicated pagination code - Drop vestigial experimentalDecorators/emitDecoratorMetadata from tsconfig.lib.json (no legacy decorators exist; the flag forced legacy semantics and broke standard-mode decorators) - Bridge Vitest 4: oxc cannot downlevel standard decorators yet, so a scoped pre-transform plugin routes decorator-bearing files through esbuild until oxc ships support
Alpaca applies one rate-limit policy across all endpoints, so a single shared retryAlpaca decorator replaces the axios-retry wiring on both axios clients (same policy: 20 retries, linear 1s backoff). shouldRetryAlpacaRequest now operates on SimplifiedHttpError instead of raw AxiosError, since the decorator sits above the simplifyError interceptor. The PDT (40310100) and short-selling (40310000) exclusions are preserved, and the transient check reuses the now-exported isTransientHttpError from retry.ts.
Extract the retry loop from the @Retry decorator into a plain withRetry() function so free functions can share the same policy machinery; the decorator is now a thin adapter over it. isEarningsDay was the last axios-retry consumer. Its finnhub client skips simplifyError, so its predicate checks the raw AxiosError (network error or 429 — same policy as before, 3 retries, linear 1s backoff). The axios-retry module mock in its test is gone since the module no longer loads retry wiring at import time.
1e1fc7f to
e91e36b
Compare
Vitest 4 transpiles tests with target "node18" unless overridden, which downlevels newer syntax. Importing the tsconfig target keeps tests on the same language level that ships in dist, instead of a second hardcoded value that can drift.
There was a problem hiding this comment.
🟡 Changes recommended
The new Vitest decorators pre-transform accepts .tsx inputs but invokes esbuild with the TS (non-JSX) loader, which can break decorator-bearing TSX files.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Refactors retry behavior in the exchange package away from axios-retry interceptors toward a TC39-standard @retry decorator (and withRetry() helper), so each endpoint can declare its own retry/backoff policy without URL-based reverse mapping.
Changes:
- Added
withRetry()+@retrydecorator (standard decorators) and a new unit test suite for the retry utility. - Migrated Trading212 and Alpaca API clients to decorator-based retry policies; updated related tests and simplified pagination retry semantics.
- Updated toolchain configuration to support standard decorators in Vitest and removed legacy decorator compiler flags; removed
axios-retryfrom dependencies/lockfile.
File summaries
| File | Description |
|---|---|
vitest.shared.ts |
Reuses library TS target for tests and adds a pre-transform plugin to downlevel standard decorators via esbuild. |
tsconfig.lib.json |
Removes legacy decorator compiler flags to avoid legacy decorator semantics. |
packages/exchange/src/util/retry.ts |
Introduces withRetry(), isTransientHttpError(), and the @retry method decorator. |
packages/exchange/src/util/retry.test.ts |
Adds tests validating retry behavior (delays, limits, non-retryables, private-field access). |
packages/exchange/src/util/isEarningsDay.ts |
Replaces axios-retry usage with withRetry() using an AxiosError-based predicate. |
packages/exchange/src/util/isEarningsDay.test.ts |
Removes axios-retry mocking no longer needed after refactor. |
packages/exchange/src/broker/trading212/api/Trading212API.ts |
Moves per-endpoint retry policies to @retry decorators; refactors history pagination to retry per-page. |
packages/exchange/src/broker/alpaca/api/AlpacaAPI.ts |
Replaces axios-retry interceptor config with a shared retryAlpaca decorator and SimplifiedHttpError-based retry predicate. |
packages/exchange/src/broker/alpaca/api/AlpacaAPI.test.ts |
Updates retry predicate tests to use SimplifiedHttpError; keeps post-order retry behavior assertions. |
packages/exchange/package.json |
Removes axios-retry dependency. |
package-lock.json |
Removes axios-retry and its transitive dependency from the lockfile. |
Review details
- Files reviewed: 10/11 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!/\.tsx?$/.test(id) || !/^\s*@[A-Za-z_$]/m.test(code)) { | ||
| return null; | ||
| } | ||
| return transform(code, {loader: 'ts', sourcefile: id, sourcemap: true, target}); |
Why
Both API clients configured retries through
axios-retryinterceptors that had to reverse-map failed request URLs/config back to policy.Trading212API'sgetRetryDelaymatched URLs via string comparison — a design that already produced one silent bug (strict===missing paginatednextPagePathURLs — see the apology comment it carried), and every new endpoint risked silently falling through to the default delay.What
@retrymethod decorator (TC39 standard mode,packages/exchange/src/util/retry.ts): each API method declares its own rate-limit policy right where the endpoint lives — no URL-matching table to keep in sync.axiosRetry.isRetryableErrorset: network errors, 429, 5xx (asSimplifiedHttpErrorstatuses0/429/5xx, sincesimplifyErrorruns at the interceptor level).getHistoryOrdersnow pages through the decoratedgetHistoryOrdersPage, preserving per-page retry semantics (a decorator on the aggregate would restart pagination) and deleting the duplicated pagination loop.AlpacaAPIconverted too (second commit): Alpaca applies one rate-limit policy across all endpoints, so a single sharedretryAlpacadecorator (20 retries, linear 1s backoff) replaces the axios-retry wiring on both axios clients.shouldRetryAlpacaRequestnow operates onSimplifiedHttpErrorinstead of rawAxiosError; the PDT (40310100) and short-selling (40310000) exclusions are preserved and covered by the existing test suite (+1 new case).Toolchain enablement:
experimentalDecorators/emitDecoratorMetadatafromtsconfig.lib.json— no legacy decorators exist in the repo, and the flag forced legacy semantics that break standard-mode decorators.vitest.config.base.ts: oxc (rolldown-vite/Vitest 4) cannot downlevel standard decorators yet, so decorator-bearing files route through esbuild (already present via tsx). Remove once oxc ships support.Behavior notes
attempt × 1s) — unchanged.ECONNABORTEDtimeouts would now retry whereaxios-retryexcluded them; moot since neither client configures a timeout.axios-retryis fully removed as a dependency (third commit): the retry loop is extracted into a plainwithRetry()that the decorator now adapts, andisEarningsDay— the last consumer — uses the function form with its original policy (network or 429, 3 retries, linear 1s backoff) against rawAxiosErrors, since its finnhub client skipssimplifyError. Itsvi.mock(.axios-retry.)workaround is gone too. (ts-retry-promisein the WebSocket managers is a different domain — left as is.)Verified
tscemit + runtime smoke test of builtdist/classtsxdemo scripts unaffected (esbuild 0.28 downlevels standard decorators)