@dofek/provider-http 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/adaptive-rate-limit.d.ts +70 -0
- package/dist/adaptive-rate-limit.d.ts.map +1 -0
- package/dist/adaptive-rate-limit.js +272 -0
- package/dist/adaptive-rate-limit.js.map +1 -0
- package/dist/rate-limit-types.d.ts +45 -0
- package/dist/rate-limit-types.d.ts.map +1 -0
- package/dist/rate-limit-types.js +37 -0
- package/dist/rate-limit-types.js.map +1 -0
- package/dist/rate-limit.d.ts +26 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +90 -0
- package/dist/rate-limit.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Asher Cohen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @dofek/provider-http
|
|
2
|
+
|
|
3
|
+
Fetch-compatible HTTP helpers for provider integrations that need typed rate-limit
|
|
4
|
+
and service-unavailable errors, `Retry-After` parsing, and optional adaptive
|
|
5
|
+
request admission.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @dofek/provider-http
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js 22.14 or newer and its built-in
|
|
14
|
+
[`fetch`](https://nodejs.org/api/globals.html#fetch) implementation.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import {
|
|
20
|
+
createRateLimitAwareFetch,
|
|
21
|
+
ProviderRateLimitError,
|
|
22
|
+
} from "@dofek/provider-http";
|
|
23
|
+
|
|
24
|
+
const providerFetch = createRateLimitAwareFetch(fetch, {
|
|
25
|
+
providerId: "example",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await providerFetch("https://api.example.com/data");
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`Provider returned HTTP ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
console.log(await response.json());
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof ProviderRateLimitError) {
|
|
36
|
+
console.log(`Retry after ${error.retryAfterSeconds ?? "an unknown number of"} seconds`);
|
|
37
|
+
} else {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Public API
|
|
44
|
+
|
|
45
|
+
| Import | Main exports |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `@dofek/provider-http` | `createRateLimitAwareFetch`, `fetchWithRateLimitHandling`, `parseRetryAfterHeader`, `isServiceUnavailableStatus`, `ProviderRateLimitError`, `ProviderServiceUnavailableError`, and their option/store types |
|
|
48
|
+
| `@dofek/provider-http/rate-limit` | Alias of the root rate-limit API |
|
|
49
|
+
| `@dofek/provider-http/adaptive-rate-limit` | Adaptive budget state, serialization, admission-delay calculations, Strava quota parsing, defaults, constants, and `AdaptiveRateLimitStore` |
|
|
50
|
+
|
|
51
|
+
`AdaptiveRateLimitStore` is an interface: applications provide persistence and
|
|
52
|
+
coordination appropriate to their runtime.
|
|
53
|
+
|
|
54
|
+
## Error and request behavior
|
|
55
|
+
|
|
56
|
+
- HTTP `429` throws `ProviderRateLimitError`.
|
|
57
|
+
- HTTP `502`, `503`, and `504` throw `ProviderServiceUnavailableError`.
|
|
58
|
+
- Both errors expose the provider, status, response body, scope, optional user,
|
|
59
|
+
and parsed `retryAfterSeconds`.
|
|
60
|
+
- Other HTTP error responses are returned unchanged, so callers must check
|
|
61
|
+
`response.ok`.
|
|
62
|
+
- These helpers do not retry automatically. The caller decides whether and when
|
|
63
|
+
to retry; an adaptive store can delay admission before a request.
|
|
64
|
+
|
|
65
|
+
No authentication or environment variables are required by this package.
|
|
66
|
+
|
|
67
|
+
## License and contributing
|
|
68
|
+
|
|
69
|
+
[MIT](./LICENSE). Source is in the
|
|
70
|
+
[Dofek repository](https://github.com/Asherlc/dofek/tree/main/packages/provider-http).
|
|
71
|
+
Please [open an issue](https://github.com/Asherlc/dofek/issues) for bugs or API
|
|
72
|
+
proposals, or [submit a pull request](https://github.com/Asherlc/dofek/pulls).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ProviderRateLimitScope } from "./rate-limit-types.ts";
|
|
2
|
+
/** Rolling window for request budget tally (5 minutes). */
|
|
3
|
+
export declare const ADAPTIVE_RATE_WINDOW_MS: number;
|
|
4
|
+
export declare const ADAPTIVE_THROTTLE_MIN_MS = 500;
|
|
5
|
+
export declare const ADAPTIVE_THROTTLE_MAX_MS = 30000;
|
|
6
|
+
export declare const ADAPTIVE_THROTTLE_DECREASE_MS = 100;
|
|
7
|
+
export declare const ADAPTIVE_THROTTLE_INCREASE_FACTOR = 2;
|
|
8
|
+
export declare const ADAPTIVE_BUDGET_SAFETY_RATIO = 0.8;
|
|
9
|
+
export declare const ADAPTIVE_DEFAULT_INFERRED_BUDGET = 40;
|
|
10
|
+
/**
|
|
11
|
+
* Minimum rolling-window requests observed at a 429 before shrinking inferred budget.
|
|
12
|
+
* Step-chain sync jobs may hit a limit after only a few HTTP calls in a fresh window;
|
|
13
|
+
* skipping early learning avoids permanent over-throttling.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ADAPTIVE_MIN_REQUESTS_FOR_BUDGET_LEARNING = 8;
|
|
16
|
+
export declare const DEFAULT_PROVIDER_THROTTLE_MS: Readonly<Record<string, number>>;
|
|
17
|
+
/** Providers that run one BullMQ job per sync step (not one job per full sync). */
|
|
18
|
+
export declare const STEP_CHAIN_SYNC_PROVIDERS: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Typical HTTP calls per step-chain sync job (auth refresh, bootstrap, one data call).
|
|
21
|
+
* Used to align BullMQ job pacing with HTTP-level adaptive budgets.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_HTTP_REQUESTS_PER_SYNC_JOB: Readonly<Record<string, number>>;
|
|
24
|
+
/** Seed inferred budgets for step-chain providers before the first 429 observation. */
|
|
25
|
+
export declare const DEFAULT_PROVIDER_INFERRED_BUDGET: Readonly<Record<string, number>>;
|
|
26
|
+
export interface StravaRateLimitQuota {
|
|
27
|
+
shortLimit: number;
|
|
28
|
+
shortUsage: number;
|
|
29
|
+
dailyLimit: number;
|
|
30
|
+
dailyUsage: number;
|
|
31
|
+
}
|
|
32
|
+
export interface ProviderAdaptiveRateState {
|
|
33
|
+
providerId: string;
|
|
34
|
+
scope: ProviderRateLimitScope;
|
|
35
|
+
userId: string | null;
|
|
36
|
+
windowStartMs: number;
|
|
37
|
+
requestCount: number;
|
|
38
|
+
throttleMs: number;
|
|
39
|
+
lastRequestMs: number | null;
|
|
40
|
+
inferredBudget: number | null;
|
|
41
|
+
observedCooldownSeconds: number | null;
|
|
42
|
+
stravaShortLimit: number | null;
|
|
43
|
+
stravaShortUsage: number | null;
|
|
44
|
+
stravaDailyLimit: number | null;
|
|
45
|
+
stravaDailyUsage: number | null;
|
|
46
|
+
}
|
|
47
|
+
export type { AdaptiveRateLimitStore } from "./rate-limit-types.ts";
|
|
48
|
+
export declare function defaultThrottleMs(providerId: string): number;
|
|
49
|
+
export declare function defaultInferredBudget(providerId: string): number | null;
|
|
50
|
+
export declare function httpRequestsPerSyncJob(providerId: string): number;
|
|
51
|
+
export declare function isStepChainSyncProvider(providerId: string): boolean;
|
|
52
|
+
export declare function parseStravaRateLimitHeaders(headers: Headers): StravaRateLimitQuota | null;
|
|
53
|
+
export declare function blendObservedCooldown(previous: number | null, observedSeconds: number): number;
|
|
54
|
+
export declare function slideAdaptiveWindow(state: ProviderAdaptiveRateState, nowMs: number): ProviderAdaptiveRateState;
|
|
55
|
+
export declare function decreaseThrottleMs(throttleMs: number): number;
|
|
56
|
+
export declare function increaseThrottleMs(throttleMs: number): number;
|
|
57
|
+
export declare function learnInferredBudget(current: number | null, requestsAtLimit: number, providerId?: string): number | null;
|
|
58
|
+
export declare function admissionDelayMs(state: ProviderAdaptiveRateState, nowMs: number): number;
|
|
59
|
+
export declare function throttleDelayMs(state: ProviderAdaptiveRateState, nowMs: number): number;
|
|
60
|
+
export declare function createInitialAdaptiveState(providerId: string, scope: ProviderRateLimitScope, userId: string | null, nowMs?: number): ProviderAdaptiveRateState;
|
|
61
|
+
export declare function applyStravaQuota(state: ProviderAdaptiveRateState, quota: StravaRateLimitQuota): ProviderAdaptiveRateState;
|
|
62
|
+
export declare function recordAdaptiveRequest(state: ProviderAdaptiveRateState, nowMs: number): ProviderAdaptiveRateState;
|
|
63
|
+
/** Updates throttle timing without consuming a step-chain sync job budget slot. */
|
|
64
|
+
export declare function recordAdaptiveThrottleTouch(state: ProviderAdaptiveRateState, nowMs: number): ProviderAdaptiveRateState;
|
|
65
|
+
export declare function recordAdaptiveRateLimit(state: ProviderAdaptiveRateState, retryAfterSeconds: number | null | undefined): ProviderAdaptiveRateState;
|
|
66
|
+
export declare const ADAPTIVE_RATE_STORAGE_KEY_PREFIX = "provider-adaptive-rate";
|
|
67
|
+
export declare function adaptiveRateLimitStorageKey(providerId: string, scope: ProviderRateLimitScope, userId: string | null): string;
|
|
68
|
+
export declare function serializeAdaptiveRateState(state: ProviderAdaptiveRateState): string;
|
|
69
|
+
export declare function parseAdaptiveRateState(raw: string | null): ProviderAdaptiveRateState | null;
|
|
70
|
+
//# sourceMappingURL=adaptive-rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adaptive-rate-limit.d.ts","sourceRoot":"","sources":["../src/adaptive-rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** Rolling window for request budget tally (5 minutes). */
|
|
2
|
+
export const ADAPTIVE_RATE_WINDOW_MS = 5 * 60 * 1000;
|
|
3
|
+
export const ADAPTIVE_THROTTLE_MIN_MS = 500;
|
|
4
|
+
export const ADAPTIVE_THROTTLE_MAX_MS = 30_000;
|
|
5
|
+
export const ADAPTIVE_THROTTLE_DECREASE_MS = 100;
|
|
6
|
+
export const ADAPTIVE_THROTTLE_INCREASE_FACTOR = 2;
|
|
7
|
+
export const ADAPTIVE_BUDGET_SAFETY_RATIO = 0.8;
|
|
8
|
+
export const ADAPTIVE_DEFAULT_INFERRED_BUDGET = 40;
|
|
9
|
+
/**
|
|
10
|
+
* Minimum rolling-window requests observed at a 429 before shrinking inferred budget.
|
|
11
|
+
* Step-chain sync jobs may hit a limit after only a few HTTP calls in a fresh window;
|
|
12
|
+
* skipping early learning avoids permanent over-throttling.
|
|
13
|
+
*/
|
|
14
|
+
export const ADAPTIVE_MIN_REQUESTS_FOR_BUDGET_LEARNING = 8;
|
|
15
|
+
export const DEFAULT_PROVIDER_THROTTLE_MS = {
|
|
16
|
+
strava: 10_000,
|
|
17
|
+
garmin: 5_000,
|
|
18
|
+
whoop: 1_000,
|
|
19
|
+
};
|
|
20
|
+
/** Providers that run one BullMQ job per sync step (not one job per full sync). */
|
|
21
|
+
export const STEP_CHAIN_SYNC_PROVIDERS = new Set(["garmin", "whoop"]);
|
|
22
|
+
/**
|
|
23
|
+
* Typical HTTP calls per step-chain sync job (auth refresh, bootstrap, one data call).
|
|
24
|
+
* Used to align BullMQ job pacing with HTTP-level adaptive budgets.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_HTTP_REQUESTS_PER_SYNC_JOB = {
|
|
27
|
+
garmin: 2,
|
|
28
|
+
whoop: 3,
|
|
29
|
+
};
|
|
30
|
+
/** Seed inferred budgets for step-chain providers before the first 429 observation. */
|
|
31
|
+
export const DEFAULT_PROVIDER_INFERRED_BUDGET = {
|
|
32
|
+
// Garmin's unofficial API is IP-based; keep the default budget conservative.
|
|
33
|
+
garmin: 20,
|
|
34
|
+
whoop: ADAPTIVE_DEFAULT_INFERRED_BUDGET,
|
|
35
|
+
};
|
|
36
|
+
export function defaultThrottleMs(providerId) {
|
|
37
|
+
return DEFAULT_PROVIDER_THROTTLE_MS[providerId] ?? 1_000;
|
|
38
|
+
}
|
|
39
|
+
export function defaultInferredBudget(providerId) {
|
|
40
|
+
return DEFAULT_PROVIDER_INFERRED_BUDGET[providerId] ?? null;
|
|
41
|
+
}
|
|
42
|
+
export function httpRequestsPerSyncJob(providerId) {
|
|
43
|
+
return DEFAULT_HTTP_REQUESTS_PER_SYNC_JOB[providerId] ?? 1;
|
|
44
|
+
}
|
|
45
|
+
export function isStepChainSyncProvider(providerId) {
|
|
46
|
+
return STEP_CHAIN_SYNC_PROVIDERS.has(providerId);
|
|
47
|
+
}
|
|
48
|
+
export function parseStravaRateLimitHeaders(headers) {
|
|
49
|
+
const limitHeader = headers.get("X-RateLimit-Limit");
|
|
50
|
+
const usageHeader = headers.get("X-RateLimit-Usage");
|
|
51
|
+
if (!limitHeader || !usageHeader)
|
|
52
|
+
return null;
|
|
53
|
+
const [shortLimitRaw, dailyLimitRaw] = limitHeader.split(",");
|
|
54
|
+
const [shortUsageRaw, dailyUsageRaw] = usageHeader.split(",");
|
|
55
|
+
const shortLimit = Number.parseInt(shortLimitRaw ?? "", 10);
|
|
56
|
+
const dailyLimit = Number.parseInt(dailyLimitRaw ?? "", 10);
|
|
57
|
+
const shortUsage = Number.parseInt(shortUsageRaw ?? "", 10);
|
|
58
|
+
const dailyUsage = Number.parseInt(dailyUsageRaw ?? "", 10);
|
|
59
|
+
if (!Number.isFinite(shortLimit) ||
|
|
60
|
+
!Number.isFinite(dailyLimit) ||
|
|
61
|
+
!Number.isFinite(shortUsage) ||
|
|
62
|
+
!Number.isFinite(dailyUsage)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return { shortLimit, shortUsage, dailyLimit, dailyUsage };
|
|
66
|
+
}
|
|
67
|
+
export function blendObservedCooldown(previous, observedSeconds) {
|
|
68
|
+
if (!Number.isFinite(observedSeconds) || observedSeconds <= 0)
|
|
69
|
+
return previous ?? observedSeconds;
|
|
70
|
+
if (previous == null)
|
|
71
|
+
return observedSeconds;
|
|
72
|
+
return Math.round(previous * 0.7 + observedSeconds * 0.3);
|
|
73
|
+
}
|
|
74
|
+
export function slideAdaptiveWindow(state, nowMs) {
|
|
75
|
+
if (nowMs - state.windowStartMs < ADAPTIVE_RATE_WINDOW_MS)
|
|
76
|
+
return state;
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
windowStartMs: nowMs,
|
|
80
|
+
requestCount: 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function decreaseThrottleMs(throttleMs) {
|
|
84
|
+
return Math.max(ADAPTIVE_THROTTLE_MIN_MS, throttleMs - ADAPTIVE_THROTTLE_DECREASE_MS);
|
|
85
|
+
}
|
|
86
|
+
export function increaseThrottleMs(throttleMs) {
|
|
87
|
+
return Math.min(ADAPTIVE_THROTTLE_MAX_MS, throttleMs * ADAPTIVE_THROTTLE_INCREASE_FACTOR);
|
|
88
|
+
}
|
|
89
|
+
export function learnInferredBudget(current, requestsAtLimit, providerId) {
|
|
90
|
+
if (requestsAtLimit <= 0)
|
|
91
|
+
return current;
|
|
92
|
+
if (requestsAtLimit < ADAPTIVE_MIN_REQUESTS_FOR_BUDGET_LEARNING) {
|
|
93
|
+
return current ?? (providerId ? defaultInferredBudget(providerId) : null);
|
|
94
|
+
}
|
|
95
|
+
const candidate = Math.max(1, requestsAtLimit - 1);
|
|
96
|
+
if (current == null)
|
|
97
|
+
return candidate;
|
|
98
|
+
return Math.min(current, candidate);
|
|
99
|
+
}
|
|
100
|
+
export function admissionDelayMs(state, nowMs) {
|
|
101
|
+
let delayMs = Math.max(throttleDelayMs(state, nowMs), budgetAdmissionDelayMs(state));
|
|
102
|
+
if (state.providerId === "strava" &&
|
|
103
|
+
state.stravaShortLimit != null &&
|
|
104
|
+
state.stravaShortUsage != null) {
|
|
105
|
+
const remaining = state.stravaShortLimit - state.stravaShortUsage;
|
|
106
|
+
if (remaining <= 2) {
|
|
107
|
+
delayMs = Math.max(delayMs, state.throttleMs * 4);
|
|
108
|
+
}
|
|
109
|
+
else if (remaining <= 5) {
|
|
110
|
+
delayMs = Math.max(delayMs, state.throttleMs * 2);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const windowMs = 15 * 60 * 1000;
|
|
114
|
+
const pacedDelay = Math.ceil(windowMs / remaining);
|
|
115
|
+
delayMs = Math.max(delayMs, Math.min(pacedDelay, ADAPTIVE_THROTTLE_MAX_MS));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return delayMs;
|
|
119
|
+
}
|
|
120
|
+
export function throttleDelayMs(state, nowMs) {
|
|
121
|
+
if (state.lastRequestMs == null)
|
|
122
|
+
return 0;
|
|
123
|
+
const elapsed = nowMs - state.lastRequestMs;
|
|
124
|
+
return elapsed < state.throttleMs ? state.throttleMs - elapsed : 0;
|
|
125
|
+
}
|
|
126
|
+
function budgetAdmissionDelayMs(state) {
|
|
127
|
+
if (state.inferredBudget == null)
|
|
128
|
+
return 0;
|
|
129
|
+
const softCap = Math.floor(state.inferredBudget * ADAPTIVE_BUDGET_SAFETY_RATIO);
|
|
130
|
+
const effectiveRequestCount = isStepChainSyncProvider(state.providerId)
|
|
131
|
+
? Math.ceil(state.requestCount / httpRequestsPerSyncJob(state.providerId))
|
|
132
|
+
: state.requestCount;
|
|
133
|
+
const effectiveSoftCap = isStepChainSyncProvider(state.providerId)
|
|
134
|
+
? Math.max(1, Math.floor(softCap / httpRequestsPerSyncJob(state.providerId)))
|
|
135
|
+
: softCap;
|
|
136
|
+
if (effectiveRequestCount >= effectiveSoftCap) {
|
|
137
|
+
return state.throttleMs;
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
export function createInitialAdaptiveState(providerId, scope, userId, nowMs = Date.now()) {
|
|
142
|
+
return {
|
|
143
|
+
providerId,
|
|
144
|
+
scope,
|
|
145
|
+
userId,
|
|
146
|
+
windowStartMs: nowMs,
|
|
147
|
+
requestCount: 0,
|
|
148
|
+
throttleMs: defaultThrottleMs(providerId),
|
|
149
|
+
lastRequestMs: null,
|
|
150
|
+
inferredBudget: defaultInferredBudget(providerId),
|
|
151
|
+
observedCooldownSeconds: null,
|
|
152
|
+
stravaShortLimit: null,
|
|
153
|
+
stravaShortUsage: null,
|
|
154
|
+
stravaDailyLimit: null,
|
|
155
|
+
stravaDailyUsage: null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function applyStravaQuota(state, quota) {
|
|
159
|
+
return {
|
|
160
|
+
...state,
|
|
161
|
+
stravaShortLimit: quota.shortLimit,
|
|
162
|
+
stravaShortUsage: quota.shortUsage,
|
|
163
|
+
stravaDailyLimit: quota.dailyLimit,
|
|
164
|
+
stravaDailyUsage: quota.dailyUsage,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function recordAdaptiveRequest(state, nowMs) {
|
|
168
|
+
const slid = slideAdaptiveWindow(state, nowMs);
|
|
169
|
+
return {
|
|
170
|
+
...slid,
|
|
171
|
+
requestCount: slid.requestCount + 1,
|
|
172
|
+
lastRequestMs: nowMs,
|
|
173
|
+
throttleMs: decreaseThrottleMs(slid.throttleMs),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** Updates throttle timing without consuming a step-chain sync job budget slot. */
|
|
177
|
+
export function recordAdaptiveThrottleTouch(state, nowMs) {
|
|
178
|
+
const slid = slideAdaptiveWindow(state, nowMs);
|
|
179
|
+
return {
|
|
180
|
+
...slid,
|
|
181
|
+
lastRequestMs: nowMs,
|
|
182
|
+
throttleMs: decreaseThrottleMs(slid.throttleMs),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function recordAdaptiveRateLimit(state, retryAfterSeconds) {
|
|
186
|
+
const observedCooldown = retryAfterSeconds != null && retryAfterSeconds > 0
|
|
187
|
+
? blendObservedCooldown(state.observedCooldownSeconds, retryAfterSeconds)
|
|
188
|
+
: state.observedCooldownSeconds;
|
|
189
|
+
return {
|
|
190
|
+
...state,
|
|
191
|
+
throttleMs: increaseThrottleMs(state.throttleMs),
|
|
192
|
+
inferredBudget: learnInferredBudget(state.inferredBudget, state.requestCount, state.providerId),
|
|
193
|
+
observedCooldownSeconds: observedCooldown,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export const ADAPTIVE_RATE_STORAGE_KEY_PREFIX = "provider-adaptive-rate";
|
|
197
|
+
export function adaptiveRateLimitStorageKey(providerId, scope, userId) {
|
|
198
|
+
return scope === "provider"
|
|
199
|
+
? `${ADAPTIVE_RATE_STORAGE_KEY_PREFIX}:${providerId}:provider`
|
|
200
|
+
: `${ADAPTIVE_RATE_STORAGE_KEY_PREFIX}:${providerId}:user:${userId ?? "unknown"}`;
|
|
201
|
+
}
|
|
202
|
+
export function serializeAdaptiveRateState(state) {
|
|
203
|
+
return JSON.stringify(state);
|
|
204
|
+
}
|
|
205
|
+
export function parseAdaptiveRateState(raw) {
|
|
206
|
+
if (!raw)
|
|
207
|
+
return null;
|
|
208
|
+
let parsed;
|
|
209
|
+
try {
|
|
210
|
+
parsed = JSON.parse(raw);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
216
|
+
return null;
|
|
217
|
+
const providerId = Reflect.get(parsed, "providerId");
|
|
218
|
+
const scope = Reflect.get(parsed, "scope");
|
|
219
|
+
const userId = Reflect.get(parsed, "userId");
|
|
220
|
+
const windowStartMs = Reflect.get(parsed, "windowStartMs");
|
|
221
|
+
const requestCount = Reflect.get(parsed, "requestCount");
|
|
222
|
+
const throttleMs = Reflect.get(parsed, "throttleMs");
|
|
223
|
+
const lastRequestMs = Reflect.get(parsed, "lastRequestMs");
|
|
224
|
+
const inferredBudget = Reflect.get(parsed, "inferredBudget");
|
|
225
|
+
const observedCooldownSeconds = Reflect.get(parsed, "observedCooldownSeconds");
|
|
226
|
+
const stravaShortLimit = Reflect.get(parsed, "stravaShortLimit");
|
|
227
|
+
const stravaShortUsage = Reflect.get(parsed, "stravaShortUsage");
|
|
228
|
+
const stravaDailyLimit = Reflect.get(parsed, "stravaDailyLimit");
|
|
229
|
+
const stravaDailyUsage = Reflect.get(parsed, "stravaDailyUsage");
|
|
230
|
+
if (typeof providerId !== "string")
|
|
231
|
+
return null;
|
|
232
|
+
if (scope !== "provider" && scope !== "user")
|
|
233
|
+
return null;
|
|
234
|
+
if (userId !== null && typeof userId !== "string")
|
|
235
|
+
return null;
|
|
236
|
+
if (typeof windowStartMs !== "number" || !Number.isFinite(windowStartMs))
|
|
237
|
+
return null;
|
|
238
|
+
if (typeof requestCount !== "number" || !Number.isFinite(requestCount))
|
|
239
|
+
return null;
|
|
240
|
+
if (typeof throttleMs !== "number" || !Number.isFinite(throttleMs))
|
|
241
|
+
return null;
|
|
242
|
+
if (lastRequestMs !== null &&
|
|
243
|
+
(typeof lastRequestMs !== "number" || !Number.isFinite(lastRequestMs))) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
providerId,
|
|
248
|
+
scope,
|
|
249
|
+
userId,
|
|
250
|
+
windowStartMs,
|
|
251
|
+
requestCount,
|
|
252
|
+
throttleMs,
|
|
253
|
+
lastRequestMs,
|
|
254
|
+
inferredBudget: typeof inferredBudget === "number" && Number.isFinite(inferredBudget) ? inferredBudget : null,
|
|
255
|
+
observedCooldownSeconds: typeof observedCooldownSeconds === "number" && Number.isFinite(observedCooldownSeconds)
|
|
256
|
+
? observedCooldownSeconds
|
|
257
|
+
: null,
|
|
258
|
+
stravaShortLimit: typeof stravaShortLimit === "number" && Number.isFinite(stravaShortLimit)
|
|
259
|
+
? stravaShortLimit
|
|
260
|
+
: null,
|
|
261
|
+
stravaShortUsage: typeof stravaShortUsage === "number" && Number.isFinite(stravaShortUsage)
|
|
262
|
+
? stravaShortUsage
|
|
263
|
+
: null,
|
|
264
|
+
stravaDailyLimit: typeof stravaDailyLimit === "number" && Number.isFinite(stravaDailyLimit)
|
|
265
|
+
? stravaDailyLimit
|
|
266
|
+
: null,
|
|
267
|
+
stravaDailyUsage: typeof stravaDailyUsage === "number" && Number.isFinite(stravaDailyUsage)
|
|
268
|
+
? stravaDailyUsage
|
|
269
|
+
: null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=adaptive-rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adaptive-rate-limit.js","sourceRoot":"","sources":["../src/adaptive-rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type ProviderRateLimitScope = "provider" | "user";
|
|
2
|
+
export type ProviderHttpErrorScope = ProviderRateLimitScope;
|
|
3
|
+
export interface ProviderRateLimitErrorOptions {
|
|
4
|
+
message: string;
|
|
5
|
+
providerId: string;
|
|
6
|
+
statusCode: number;
|
|
7
|
+
responseBody: string;
|
|
8
|
+
scope?: ProviderRateLimitScope;
|
|
9
|
+
userId?: string | null;
|
|
10
|
+
retryAfterSeconds?: number | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ProviderServiceUnavailableErrorOptions {
|
|
13
|
+
message: string;
|
|
14
|
+
providerId: string;
|
|
15
|
+
statusCode: number;
|
|
16
|
+
responseBody: string;
|
|
17
|
+
scope?: ProviderHttpErrorScope;
|
|
18
|
+
userId?: string | null;
|
|
19
|
+
retryAfterSeconds?: number | null;
|
|
20
|
+
}
|
|
21
|
+
export declare class ProviderRateLimitError extends Error {
|
|
22
|
+
readonly providerId: string;
|
|
23
|
+
readonly statusCode: number;
|
|
24
|
+
readonly responseBody: string;
|
|
25
|
+
readonly scope: ProviderRateLimitScope;
|
|
26
|
+
readonly userId: string | null;
|
|
27
|
+
readonly retryAfterSeconds: number | null;
|
|
28
|
+
constructor(options: ProviderRateLimitErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
export declare class ProviderServiceUnavailableError extends Error {
|
|
31
|
+
readonly providerId: string;
|
|
32
|
+
readonly statusCode: number;
|
|
33
|
+
readonly responseBody: string;
|
|
34
|
+
readonly scope: ProviderHttpErrorScope;
|
|
35
|
+
readonly userId: string | null;
|
|
36
|
+
readonly retryAfterSeconds: number | null;
|
|
37
|
+
constructor(options: ProviderServiceUnavailableErrorOptions);
|
|
38
|
+
}
|
|
39
|
+
export interface AdaptiveRateLimitStore {
|
|
40
|
+
awaitAdmission(providerId: string, scope: ProviderRateLimitScope, userId: string | null): Promise<void>;
|
|
41
|
+
recordSuccess(providerId: string, scope: ProviderRateLimitScope, userId: string | null, responseHeaders?: Headers): Promise<void>;
|
|
42
|
+
recordRateLimit(error: ProviderRateLimitError): Promise<void>;
|
|
43
|
+
getLearnedCooldownSeconds(providerId: string): Promise<number | null>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=rate-limit-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-types.d.ts","sourceRoot":"","sources":["../src/rate-limit-types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,MAAM,CAAC;AACzD,MAAM,MAAM,sBAAsB,GAAG,sBAAsB,CAAC;AAE5D,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,sCAAsC;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;gBAE9B,OAAO,EAAE,6BAA6B;CAUnD;AAED,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,sBAAsB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;gBAE9B,OAAO,EAAE,sCAAsC;CAU5D;AAED,MAAM,WAAW,sBAAsB;IACrC,cAAc,CACZ,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,sBAAsB,EAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,sBAAsB,EAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACvE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class ProviderRateLimitError extends Error {
|
|
2
|
+
providerId;
|
|
3
|
+
statusCode;
|
|
4
|
+
responseBody;
|
|
5
|
+
scope;
|
|
6
|
+
userId;
|
|
7
|
+
retryAfterSeconds;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
super(options.message);
|
|
10
|
+
this.name = "ProviderRateLimitError";
|
|
11
|
+
this.providerId = options.providerId;
|
|
12
|
+
this.statusCode = options.statusCode;
|
|
13
|
+
this.responseBody = options.responseBody;
|
|
14
|
+
this.scope = options.scope ?? "provider";
|
|
15
|
+
this.userId = options.userId ?? null;
|
|
16
|
+
this.retryAfterSeconds = options.retryAfterSeconds ?? null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ProviderServiceUnavailableError extends Error {
|
|
20
|
+
providerId;
|
|
21
|
+
statusCode;
|
|
22
|
+
responseBody;
|
|
23
|
+
scope;
|
|
24
|
+
userId;
|
|
25
|
+
retryAfterSeconds;
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super(options.message);
|
|
28
|
+
this.name = "ProviderServiceUnavailableError";
|
|
29
|
+
this.providerId = options.providerId;
|
|
30
|
+
this.statusCode = options.statusCode;
|
|
31
|
+
this.responseBody = options.responseBody;
|
|
32
|
+
this.scope = options.scope ?? "provider";
|
|
33
|
+
this.userId = options.userId ?? null;
|
|
34
|
+
this.retryAfterSeconds = options.retryAfterSeconds ?? null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=rate-limit-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-types.js","sourceRoot":"","sources":["../src/rate-limit-types.ts"],"names":[],"mappings":"AAuBA,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IACtC,UAAU,CAAS;IACnB,UAAU,CAAS;IACnB,YAAY,CAAS;IACrB,KAAK,CAAyB;IAC9B,MAAM,CAAgB;IACtB,iBAAiB,CAAgB;IAE1C,YAAY,OAAsC;QAChD,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QACrC,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,IAAI,CAAC;IAC7D,CAAC;CACF;AAED,MAAM,OAAO,+BAAgC,SAAQ,KAAK;IAC/C,UAAU,CAAS;IACnB,UAAU,CAAS;IACnB,YAAY,CAAS;IACrB,KAAK,CAAyB;IAC9B,MAAM,CAAgB;IACtB,iBAAiB,CAAgB;IAE1C,YAAY,OAA+C;QACzD,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,iCAAiC,CAAC;QAC9C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QACzC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC;QACzC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QACrC,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,IAAI,IAAI,CAAC;IAC7D,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AdaptiveRateLimitStore } from "./rate-limit-types.ts";
|
|
2
|
+
import { ProviderRateLimitError, type ProviderRateLimitScope, ProviderServiceUnavailableError } from "./rate-limit-types.ts";
|
|
3
|
+
export type { AdaptiveRateLimitStore, ProviderHttpErrorScope, ProviderRateLimitErrorOptions, ProviderRateLimitScope, ProviderServiceUnavailableErrorOptions, } from "./rate-limit-types.ts";
|
|
4
|
+
export { ProviderRateLimitError, ProviderServiceUnavailableError };
|
|
5
|
+
export interface FetchRateLimitHandlingOptions {
|
|
6
|
+
createRateLimitError: (response: Response, responseBody: string) => Error;
|
|
7
|
+
createServiceUnavailableError?: (response: Response, responseBody: string) => Error;
|
|
8
|
+
}
|
|
9
|
+
export interface RateLimitAwareFetchOptions {
|
|
10
|
+
providerId: string;
|
|
11
|
+
scope?: ProviderRateLimitScope;
|
|
12
|
+
userId?: string | null;
|
|
13
|
+
createRateLimitError?: (response: Response, responseBody: string) => Error;
|
|
14
|
+
createServiceUnavailableError?: (response: Response, responseBody: string) => Error;
|
|
15
|
+
adaptiveStore?: AdaptiveRateLimitStore;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parses an HTTP `Retry-After` header value, which may be either a number of
|
|
19
|
+
* seconds or an HTTP-date. Returns the delay in seconds, or null when absent or
|
|
20
|
+
* unparseable.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseRetryAfterHeader(header: string | null | undefined): number | null;
|
|
23
|
+
export declare function isServiceUnavailableStatus(statusCode: number): boolean;
|
|
24
|
+
export declare function fetchWithRateLimitHandling(fetchFn: typeof globalThis.fetch, input: RequestInfo | URL, init: RequestInit | undefined, options: FetchRateLimitHandlingOptions): Promise<Response>;
|
|
25
|
+
export declare function createRateLimitAwareFetch(fetchFn: typeof globalThis.fetch, options: RateLimitAwareFetchOptions): typeof globalThis.fetch;
|
|
26
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAEL,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,+BAA+B,EAChC,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,sBAAsB,EACtB,sBAAsB,EACtB,6BAA6B,EAC7B,sBAAsB,EACtB,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,sBAAsB,EAAE,+BAA+B,EAAE,CAAC;AAEnE,MAAM,WAAW,6BAA6B;IAC5C,oBAAoB,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,KAAK,KAAK,CAAC;IAC1E,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,KAAK,KAAK,CAAC;CACrF;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oBAAoB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,KAAK,KAAK,CAAC;IAC3E,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,KAAK,KAAK,CAAC;IACpF,aAAa,CAAC,EAAE,sBAAsB,CAAC;CACxC;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUtF;AAED,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,UAAU,CAAC,KAAK,EAChC,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,QAAQ,CAAC,CAqBnB;AAsCD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,UAAU,CAAC,KAAK,EAChC,OAAO,EAAE,0BAA0B,GAClC,OAAO,UAAU,CAAC,KAAK,CA+CzB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ProviderRateLimitError, ProviderServiceUnavailableError, } from "./rate-limit-types.js";
|
|
2
|
+
export { ProviderRateLimitError, ProviderServiceUnavailableError };
|
|
3
|
+
const rateLimitAwareFetches = new WeakSet();
|
|
4
|
+
/**
|
|
5
|
+
* Parses an HTTP `Retry-After` header value, which may be either a number of
|
|
6
|
+
* seconds or an HTTP-date. Returns the delay in seconds, or null when absent or
|
|
7
|
+
* unparseable.
|
|
8
|
+
*/
|
|
9
|
+
export function parseRetryAfterHeader(header) {
|
|
10
|
+
if (!header)
|
|
11
|
+
return null;
|
|
12
|
+
const seconds = Number.parseInt(header, 10);
|
|
13
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
14
|
+
return seconds;
|
|
15
|
+
const retryAt = Date.parse(header);
|
|
16
|
+
if (Number.isNaN(retryAt))
|
|
17
|
+
return null;
|
|
18
|
+
return Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));
|
|
19
|
+
}
|
|
20
|
+
export function isServiceUnavailableStatus(statusCode) {
|
|
21
|
+
return statusCode === 502 || statusCode === 503 || statusCode === 504;
|
|
22
|
+
}
|
|
23
|
+
export async function fetchWithRateLimitHandling(fetchFn, input, init, options) {
|
|
24
|
+
const response = init === undefined ? await fetchFn(input) : await fetchFn(input, init);
|
|
25
|
+
if (response.status === 429) {
|
|
26
|
+
const responseBody = await response.text();
|
|
27
|
+
throw options.createRateLimitError(response, responseBody);
|
|
28
|
+
}
|
|
29
|
+
if (isServiceUnavailableStatus(response.status)) {
|
|
30
|
+
const responseBody = await response.text();
|
|
31
|
+
const createServiceUnavailableError = options.createServiceUnavailableError ??
|
|
32
|
+
((unavailableResponse, unavailableResponseBody) => createDefaultServiceUnavailableError("unknown", "provider", null, unavailableResponse, unavailableResponseBody));
|
|
33
|
+
throw createServiceUnavailableError(response, responseBody);
|
|
34
|
+
}
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
function createDefaultRateLimitError(providerId, scope, userId, response, responseBody) {
|
|
38
|
+
return new ProviderRateLimitError({
|
|
39
|
+
message: `${providerId} API rate limit exceeded (${response.status}): ${responseBody}`,
|
|
40
|
+
providerId,
|
|
41
|
+
statusCode: response.status,
|
|
42
|
+
responseBody,
|
|
43
|
+
scope,
|
|
44
|
+
userId,
|
|
45
|
+
retryAfterSeconds: parseRetryAfterHeader(response.headers.get("Retry-After")),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function createDefaultServiceUnavailableError(providerId, scope, userId, response, responseBody) {
|
|
49
|
+
return new ProviderServiceUnavailableError({
|
|
50
|
+
message: `${providerId} API service unavailable (${response.status}): ${responseBody}`,
|
|
51
|
+
providerId,
|
|
52
|
+
statusCode: response.status,
|
|
53
|
+
responseBody,
|
|
54
|
+
scope,
|
|
55
|
+
userId,
|
|
56
|
+
retryAfterSeconds: parseRetryAfterHeader(response.headers.get("Retry-After")),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function createRateLimitAwareFetch(fetchFn, options) {
|
|
60
|
+
if (rateLimitAwareFetches.has(fetchFn))
|
|
61
|
+
return fetchFn;
|
|
62
|
+
const rateLimitFetch = async (input, init) => {
|
|
63
|
+
const scope = options.scope ?? "provider";
|
|
64
|
+
const userId = options.userId ?? null;
|
|
65
|
+
if (options.adaptiveStore) {
|
|
66
|
+
await options.adaptiveStore.awaitAdmission(options.providerId, scope, userId);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetchWithRateLimitHandling(fetchFn, input, init, {
|
|
70
|
+
createRateLimitError: options.createRateLimitError ??
|
|
71
|
+
((response, responseBody) => createDefaultRateLimitError(options.providerId, scope, userId, response, responseBody)),
|
|
72
|
+
createServiceUnavailableError: options.createServiceUnavailableError ??
|
|
73
|
+
((response, responseBody) => createDefaultServiceUnavailableError(options.providerId, scope, userId, response, responseBody)),
|
|
74
|
+
});
|
|
75
|
+
if (options.adaptiveStore && response.ok) {
|
|
76
|
+
await options.adaptiveStore.recordSuccess(options.providerId, scope, userId, response.headers);
|
|
77
|
+
}
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (options.adaptiveStore && err instanceof ProviderRateLimitError) {
|
|
82
|
+
await options.adaptiveStore.recordRateLimit(err);
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
rateLimitAwareFetches.add(rateLimitFetch);
|
|
88
|
+
return rateLimitFetch;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dofek/provider-http",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rate-limit-aware Fetch utilities and typed provider HTTP errors",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Asherlc/dofek.git",
|
|
10
|
+
"directory": "packages/provider-http"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Asherlc/dofek/tree/main/packages/provider-http#readme",
|
|
13
|
+
"bugs": "https://github.com/Asherlc/dofek/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"fetch",
|
|
16
|
+
"http",
|
|
17
|
+
"rate-limit",
|
|
18
|
+
"retry-after",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.14.0"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/rate-limit.d.ts",
|
|
33
|
+
"import": "./dist/rate-limit.js"
|
|
34
|
+
},
|
|
35
|
+
"./rate-limit": {
|
|
36
|
+
"types": "./dist/rate-limit.d.ts",
|
|
37
|
+
"import": "./dist/rate-limit.js"
|
|
38
|
+
},
|
|
39
|
+
"./adaptive-rate-limit": {
|
|
40
|
+
"types": "./dist/adaptive-rate-limit.d.ts",
|
|
41
|
+
"import": "./dist/adaptive-rate-limit.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"prepack": "pnpm run build",
|
|
50
|
+
"test": "vitest run src",
|
|
51
|
+
"typecheck": "tsc --noEmit"
|
|
52
|
+
},
|
|
53
|
+
"gitHead": "ac95258c753c7e75b57da7ae47426d22b78e8c01"
|
|
54
|
+
}
|