Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
002a33c
Improve provider sync rate limiting for Garmin and Whoop.
Asherlc Jun 21, 2026
12abef9
Fix CI typecheck and import boundary failures.
Asherlc Jun 21, 2026
6c1a3b4
Add adaptive store tests for rate-limit mutation coverage.
Asherlc Jun 21, 2026
d53ac4f
Fix lint and Stryker CI failures for rate-limit changes.
Asherlc Jun 21, 2026
f7eed45
Fix Strava test signatures after removing client throttle.
Asherlc Jun 21, 2026
a3470ce
Fix vitest import order in cooldown test.
Asherlc Jun 21, 2026
41008cc
Expand adaptive rate-limit mutation coverage and fix Whoop daily sync.
Asherlc Jun 21, 2026
c71a77b
Merge branch 'main' into Asherlc/garmin-sync-429-strategy
Asherlc Jun 21, 2026
2588080
Simplify Whoop rate-limit detection to use shared base class.
Asherlc Jun 21, 2026
75ed2a8
Fix CI failures for lint, tests, and mutation coverage.
Asherlc Jun 21, 2026
2ca7fd7
Add WHOOP sync helper tests for mutation coverage.
Asherlc Jun 21, 2026
911bc2a
Fix typecheck in WHOOP sync helper tests.
Asherlc Jun 21, 2026
cd19b7c
Address CodeRabbit review feedback on rate limiting.
Asherlc Jun 21, 2026
202e92b
Use atomic Redis admission for adaptive rate limiting.
Asherlc Jun 21, 2026
092abd1
Fix provider rate-limit tests after single-wrapper refactor.
Asherlc Jun 21, 2026
3efe97c
Fix CI lint failures and improve rate-limit mutation coverage.
Asherlc Jun 21, 2026
3995d95
Add mutation tests for MCP sync cooldown and atomic Redis admission.
Asherlc Jun 22, 2026
c20f28b
Merge origin/main into garmin-sync-429-strategy branch.
Asherlc Jun 22, 2026
f384667
Address remaining CodeRabbit review feedback on rate limiting.
Asherlc Jun 22, 2026
a83435d
Fix CI lint and typecheck failures in provider tests.
Asherlc Jun 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 13 additions & 19 deletions packages/garmin-connect/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@
* 6. All API calls use OAuth2 Bearer token
*/

import {
createRateLimitAwareFetch,
ProviderRateLimitError,
parseRetryAfterHeader,
} from "@dofek/provider-http/rate-limit";
import { ProviderRateLimitError, parseRetryAfterHeader } from "@dofek/provider-http/rate-limit";
import { buildOAuth1Header } from "./oauth1.ts";
import type {
BodyBatteryDay,
Expand Down Expand Up @@ -45,6 +41,10 @@ const OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.jso
const USER_AGENT = "com.garmin.android.apps.connectmobile";
const API_USER_AGENT = "GCM-iOS-5.19.1.2";

/** Minimum delay between consecutive Connect API requests (ms).
* Garmin's unofficial API rate limits aggressively; 2s keeps bursts under control. */
export const GARMIN_CONNECT_THROTTLE_MS = 2_000;

const CSRF_RE = /name="_csrf"\s+value="(.+?)"/;
const TITLE_RE = /<title>(.+?)<\/title>/;
const TICKET_RE = /embed\?ticket=([^"]+)"/;
Expand All @@ -56,18 +56,9 @@ export class GarminConnectClient {
#displayName: string | null = null;
#domain: string;
#fetchFn: typeof globalThis.fetch;

constructor(domain: string = "garmin.com", fetchFn: typeof globalThis.fetch = globalThis.fetch) {
this.#domain = domain;
this.#fetchFn = createRateLimitAwareFetch(fetchFn, {
providerId: "garmin",
createRateLimitError: (response, responseBody) =>
new GarminRateLimitError(
`Rate limit exceeded (${response.status}): ${responseBody}`,
responseBody,
response.headers?.get?.("Retry-After"),
),
});
this.#fetchFn = fetchFn;
}

// ============================================================
Expand Down Expand Up @@ -346,12 +337,15 @@ export class GarminConnectClient {
throw new GarminAuthError("Authentication failed (401)");
}

if (response.status === 429) {
throw new GarminRateLimitError("Rate limit exceeded (429)");
}

if (!response.ok) {
const text = await response.text();
if (response.status === 429) {
throw new GarminRateLimitError(
`Rate limit exceeded (${response.status}): ${text}`,
text,
response.headers?.get?.("Retry-After"),
);
}
throw new GarminApiError(`API error (${response.status}): ${text}`, response.status);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/provider-http/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"type": "module",
"exports": {
"./rate-limit": "./src/rate-limit.ts"
"./rate-limit": "./src/rate-limit.ts",
"./adaptive-rate-limit": "./src/adaptive-rate-limit.ts"
}
}
Loading
Loading