@pinta365/strava 0.0.1
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 +390 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/deps/jsr.io/@cross/runtime/1.2.1/mod.d.ts +126 -0
- package/esm/deps/jsr.io/@cross/runtime/1.2.1/mod.js +480 -0
- package/esm/mod.d.ts +27 -0
- package/esm/mod.js +27 -0
- package/esm/package.json +3 -0
- package/esm/src/auth/oauth.d.ts +68 -0
- package/esm/src/auth/oauth.js +203 -0
- package/esm/src/auth/scopes.d.ts +52 -0
- package/esm/src/auth/scopes.js +71 -0
- package/esm/src/auth/token-store.d.ts +57 -0
- package/esm/src/auth/token-store.js +142 -0
- package/esm/src/client.d.ts +98 -0
- package/esm/src/client.js +235 -0
- package/esm/src/errors.d.ts +52 -0
- package/esm/src/errors.js +102 -0
- package/esm/src/http/deduplication.d.ts +33 -0
- package/esm/src/http/deduplication.js +96 -0
- package/esm/src/http/rate-limiter.d.ts +47 -0
- package/esm/src/http/rate-limiter.js +168 -0
- package/esm/src/http/request.d.ts +24 -0
- package/esm/src/http/request.js +158 -0
- package/esm/src/http/retry.d.ts +9 -0
- package/esm/src/http/retry.js +61 -0
- package/esm/src/resources/activities.d.ts +149 -0
- package/esm/src/resources/activities.js +189 -0
- package/esm/src/resources/athletes.d.ts +37 -0
- package/esm/src/resources/athletes.js +85 -0
- package/esm/src/resources/clubs.d.ts +45 -0
- package/esm/src/resources/clubs.js +71 -0
- package/esm/src/resources/gears.d.ts +17 -0
- package/esm/src/resources/gears.js +27 -0
- package/esm/src/resources/routes.d.ts +33 -0
- package/esm/src/resources/routes.js +71 -0
- package/esm/src/resources/segment-efforts.d.ts +38 -0
- package/esm/src/resources/segment-efforts.js +53 -0
- package/esm/src/resources/segments.d.ts +42 -0
- package/esm/src/resources/segments.js +67 -0
- package/esm/src/resources/streams.d.ts +44 -0
- package/esm/src/resources/streams.js +75 -0
- package/esm/src/resources/uploads.d.ts +41 -0
- package/esm/src/resources/uploads.js +79 -0
- package/esm/src/types/api.d.ts +9 -0
- package/esm/src/types/api.js +7 -0
- package/esm/src/types/common.d.ts +65 -0
- package/esm/src/types/common.js +4 -0
- package/esm/src/types/generated.d.ts +731 -0
- package/esm/src/types/generated.js +7 -0
- package/esm/src/utils/pagination.d.ts +45 -0
- package/esm/src/utils/pagination.js +112 -0
- package/esm/src/utils/transformers.d.ts +30 -0
- package/esm/src/utils/transformers.js +189 -0
- package/esm/src/utils/validators.d.ts +53 -0
- package/esm/src/utils/validators.js +84 -0
- package/package.json +40 -0
- package/script/_dnt.shims.d.ts +2 -0
- package/script/_dnt.shims.js +60 -0
- package/script/deps/jsr.io/@cross/runtime/1.2.1/mod.d.ts +126 -0
- package/script/deps/jsr.io/@cross/runtime/1.2.1/mod.js +526 -0
- package/script/mod.d.ts +27 -0
- package/script/mod.js +73 -0
- package/script/package.json +3 -0
- package/script/src/auth/oauth.d.ts +68 -0
- package/script/src/auth/oauth.js +211 -0
- package/script/src/auth/scopes.d.ts +52 -0
- package/script/src/auth/scopes.js +79 -0
- package/script/src/auth/token-store.d.ts +57 -0
- package/script/src/auth/token-store.js +182 -0
- package/script/src/client.d.ts +98 -0
- package/script/src/client.js +239 -0
- package/script/src/errors.d.ts +52 -0
- package/script/src/errors.js +111 -0
- package/script/src/http/deduplication.d.ts +33 -0
- package/script/src/http/deduplication.js +100 -0
- package/script/src/http/rate-limiter.d.ts +47 -0
- package/script/src/http/rate-limiter.js +172 -0
- package/script/src/http/request.d.ts +24 -0
- package/script/src/http/request.js +161 -0
- package/script/src/http/retry.d.ts +9 -0
- package/script/src/http/retry.js +64 -0
- package/script/src/resources/activities.d.ts +149 -0
- package/script/src/resources/activities.js +193 -0
- package/script/src/resources/athletes.d.ts +37 -0
- package/script/src/resources/athletes.js +89 -0
- package/script/src/resources/clubs.d.ts +45 -0
- package/script/src/resources/clubs.js +75 -0
- package/script/src/resources/gears.d.ts +17 -0
- package/script/src/resources/gears.js +31 -0
- package/script/src/resources/routes.d.ts +33 -0
- package/script/src/resources/routes.js +75 -0
- package/script/src/resources/segment-efforts.d.ts +38 -0
- package/script/src/resources/segment-efforts.js +57 -0
- package/script/src/resources/segments.d.ts +42 -0
- package/script/src/resources/segments.js +71 -0
- package/script/src/resources/streams.d.ts +44 -0
- package/script/src/resources/streams.js +79 -0
- package/script/src/resources/uploads.d.ts +41 -0
- package/script/src/resources/uploads.js +83 -0
- package/script/src/types/api.d.ts +9 -0
- package/script/src/types/api.js +23 -0
- package/script/src/types/common.d.ts +65 -0
- package/script/src/types/common.js +5 -0
- package/script/src/types/generated.d.ts +731 -0
- package/script/src/types/generated.js +8 -0
- package/script/src/utils/pagination.d.ts +45 -0
- package/script/src/utils/pagination.js +118 -0
- package/script/src/utils/transformers.d.ts +30 -0
- package/script/src/utils/transformers.js +196 -0
- package/script/src/utils/validators.d.ts +53 -0
- package/script/src/utils/validators.js +92 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request deduplication to prevent duplicate requests within a time window
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Request deduplication cache
|
|
6
|
+
*/
|
|
7
|
+
export declare class RequestDeduplicator {
|
|
8
|
+
private cache;
|
|
9
|
+
private cleanupInterval;
|
|
10
|
+
private readonly windowMs;
|
|
11
|
+
constructor(windowMs?: number);
|
|
12
|
+
/**
|
|
13
|
+
* Generate cache key from request details
|
|
14
|
+
*/
|
|
15
|
+
private generateKey;
|
|
16
|
+
/**
|
|
17
|
+
* Get or create a request promise
|
|
18
|
+
*/
|
|
19
|
+
getOrCreate<T>(method: string, path: string, query: Record<string, unknown> | undefined, body: unknown, factory: () => Promise<T>): Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* Clear expired entries
|
|
22
|
+
*/
|
|
23
|
+
private cleanup;
|
|
24
|
+
/**
|
|
25
|
+
* Start periodic cleanup
|
|
26
|
+
*/
|
|
27
|
+
private startCleanup;
|
|
28
|
+
/**
|
|
29
|
+
* Stop cleanup and clear cache
|
|
30
|
+
*/
|
|
31
|
+
destroy(): void;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=deduplication.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Request deduplication to prevent duplicate requests within a time window
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RequestDeduplicator = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Request deduplication cache
|
|
9
|
+
*/
|
|
10
|
+
class RequestDeduplicator {
|
|
11
|
+
constructor(windowMs = 5000) {
|
|
12
|
+
Object.defineProperty(this, "cache", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: new Map()
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "cleanupInterval", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: null
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "windowMs", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
this.windowMs = windowMs;
|
|
31
|
+
this.startCleanup();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate cache key from request details
|
|
35
|
+
*/
|
|
36
|
+
generateKey(method, path, query, body) {
|
|
37
|
+
const parts = [method, path];
|
|
38
|
+
if (query) {
|
|
39
|
+
const sortedQuery = Object.keys(query)
|
|
40
|
+
.sort()
|
|
41
|
+
.map((key) => `${key}=${String(query[key])}`)
|
|
42
|
+
.join("&");
|
|
43
|
+
parts.push(sortedQuery);
|
|
44
|
+
}
|
|
45
|
+
if (body) {
|
|
46
|
+
try {
|
|
47
|
+
const bodyHash = JSON.stringify(body);
|
|
48
|
+
parts.push(bodyHash);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
parts.push(String(body));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parts.join("|");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get or create a request promise
|
|
58
|
+
*/
|
|
59
|
+
getOrCreate(method, path, query, body, factory) {
|
|
60
|
+
const key = this.generateKey(method, path, query, body);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const cached = this.cache.get(key);
|
|
63
|
+
if (cached && (now - cached.timestamp) < this.windowMs) {
|
|
64
|
+
return cached.promise;
|
|
65
|
+
}
|
|
66
|
+
const promise = factory();
|
|
67
|
+
this.cache.set(key, { promise, timestamp: now });
|
|
68
|
+
return promise;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Clear expired entries
|
|
72
|
+
*/
|
|
73
|
+
cleanup() {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
76
|
+
if (now - entry.timestamp >= this.windowMs) {
|
|
77
|
+
this.cache.delete(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Start periodic cleanup
|
|
83
|
+
*/
|
|
84
|
+
startCleanup() {
|
|
85
|
+
this.cleanupInterval = setInterval(() => {
|
|
86
|
+
this.cleanup();
|
|
87
|
+
}, this.windowMs);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Stop cleanup and clear cache
|
|
91
|
+
*/
|
|
92
|
+
destroy() {
|
|
93
|
+
if (this.cleanupInterval !== null) {
|
|
94
|
+
clearInterval(this.cleanupInterval);
|
|
95
|
+
this.cleanupInterval = null;
|
|
96
|
+
}
|
|
97
|
+
this.cache.clear();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
exports.RequestDeduplicator = RequestDeduplicator;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limit tracking and queue management
|
|
3
|
+
*/
|
|
4
|
+
import type { RateLimitInfo, RateLimitStrategy } from "../types/common.js";
|
|
5
|
+
/**
|
|
6
|
+
* Rate limiter for Strava API
|
|
7
|
+
*/
|
|
8
|
+
export declare class RateLimiter {
|
|
9
|
+
private shortTermLimit;
|
|
10
|
+
private shortTermWindow;
|
|
11
|
+
private dailyLimit;
|
|
12
|
+
private dailyWindow;
|
|
13
|
+
private shortTermRequests;
|
|
14
|
+
private dailyRequests;
|
|
15
|
+
private queue;
|
|
16
|
+
private strategy;
|
|
17
|
+
constructor(strategy?: RateLimitStrategy);
|
|
18
|
+
/**
|
|
19
|
+
* Update rate limit info from response headers
|
|
20
|
+
*/
|
|
21
|
+
updateFromHeaders(headers: Headers): void;
|
|
22
|
+
/**
|
|
23
|
+
* Record a request timestamp
|
|
24
|
+
*/
|
|
25
|
+
private recordRequest;
|
|
26
|
+
/**
|
|
27
|
+
* Get current rate limit info
|
|
28
|
+
*/
|
|
29
|
+
getRateLimitInfo(): RateLimitInfo;
|
|
30
|
+
/**
|
|
31
|
+
* Check if we can make a request
|
|
32
|
+
*/
|
|
33
|
+
canMakeRequest(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Wait until we can make a request
|
|
36
|
+
*/
|
|
37
|
+
waitForAvailability(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Process queued requests
|
|
40
|
+
*/
|
|
41
|
+
processQueue(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Set rate limit strategy
|
|
44
|
+
*/
|
|
45
|
+
setStrategy(strategy: RateLimitStrategy): void;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rate limit tracking and queue management
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RateLimiter = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Rate limiter for Strava API
|
|
9
|
+
*/
|
|
10
|
+
class RateLimiter {
|
|
11
|
+
constructor(strategy = "queue") {
|
|
12
|
+
Object.defineProperty(this, "shortTermLimit", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: 600
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "shortTermWindow", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: 15 * 60 * 1000
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "dailyLimit", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: 30000
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "dailyWindow", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: 24 * 60 * 60 * 1000
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(this, "shortTermRequests", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
value: []
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(this, "dailyRequests", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: []
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, "queue", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: []
|
|
53
|
+
});
|
|
54
|
+
Object.defineProperty(this, "strategy", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
writable: true,
|
|
58
|
+
value: void 0
|
|
59
|
+
});
|
|
60
|
+
this.strategy = strategy;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Update rate limit info from response headers
|
|
64
|
+
*/
|
|
65
|
+
updateFromHeaders(headers) {
|
|
66
|
+
const shortTermLimit = headers.get("X-RateLimit-Limit");
|
|
67
|
+
const shortTermUsage = headers.get("X-RateLimit-Usage");
|
|
68
|
+
const dailyLimit = headers.get("X-RateLimit-Limit-Daily");
|
|
69
|
+
const dailyUsage = headers.get("X-RateLimit-Usage-Daily");
|
|
70
|
+
if (shortTermLimit) {
|
|
71
|
+
this.shortTermLimit = parseInt(shortTermLimit, 10);
|
|
72
|
+
}
|
|
73
|
+
if (shortTermUsage) {
|
|
74
|
+
const parts = shortTermUsage.split(",");
|
|
75
|
+
if (parts.length >= 1) {
|
|
76
|
+
const current = parseInt(parts[0], 10);
|
|
77
|
+
this.recordRequest("shortTerm", current);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (dailyLimit) {
|
|
81
|
+
this.dailyLimit = parseInt(dailyLimit, 10);
|
|
82
|
+
}
|
|
83
|
+
if (dailyUsage) {
|
|
84
|
+
const parts = dailyUsage.split(",");
|
|
85
|
+
if (parts.length >= 1) {
|
|
86
|
+
const current = parseInt(parts[0], 10);
|
|
87
|
+
this.recordRequest("daily", current);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Record a request timestamp
|
|
93
|
+
*/
|
|
94
|
+
recordRequest(type, _currentUsage) {
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const window = type === "shortTerm" ? this.shortTermWindow : this.dailyWindow;
|
|
97
|
+
const requests = type === "shortTerm" ? this.shortTermRequests : this.dailyRequests;
|
|
98
|
+
const cutoff = now - window;
|
|
99
|
+
while (requests.length > 0 && requests[0] < cutoff) {
|
|
100
|
+
requests.shift();
|
|
101
|
+
}
|
|
102
|
+
requests.push(now);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get current rate limit info
|
|
106
|
+
*/
|
|
107
|
+
getRateLimitInfo() {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const shortTermCutoff = now - this.shortTermWindow;
|
|
110
|
+
this.shortTermRequests = this.shortTermRequests.filter((t) => t >= shortTermCutoff);
|
|
111
|
+
const dailyCutoff = now - this.dailyWindow;
|
|
112
|
+
this.dailyRequests = this.dailyRequests.filter((t) => t >= dailyCutoff);
|
|
113
|
+
return {
|
|
114
|
+
shortTermLimit: this.shortTermLimit,
|
|
115
|
+
shortTermUsage: this.shortTermRequests.length,
|
|
116
|
+
dailyLimit: this.dailyLimit,
|
|
117
|
+
dailyUsage: this.dailyRequests.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if we can make a request
|
|
122
|
+
*/
|
|
123
|
+
canMakeRequest() {
|
|
124
|
+
const info = this.getRateLimitInfo();
|
|
125
|
+
return (info.shortTermUsage ?? 0) < (info.shortTermLimit ?? 600) &&
|
|
126
|
+
(info.dailyUsage ?? 0) < (info.dailyLimit ?? 30000);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Wait until we can make a request
|
|
130
|
+
*/
|
|
131
|
+
async waitForAvailability() {
|
|
132
|
+
if (this.canMakeRequest()) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.strategy === "throw") {
|
|
136
|
+
const info = this.getRateLimitInfo();
|
|
137
|
+
throw new Error(`Rate limit exceeded. Short-term: ${info.shortTermUsage}/${info.shortTermLimit}, ` +
|
|
138
|
+
`Daily: ${info.dailyUsage}/${info.dailyLimit}`);
|
|
139
|
+
}
|
|
140
|
+
if (this.strategy === "wait") {
|
|
141
|
+
while (!this.canMakeRequest()) {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
this.queue.push({
|
|
148
|
+
resolve,
|
|
149
|
+
reject,
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Process queued requests
|
|
156
|
+
*/
|
|
157
|
+
processQueue() {
|
|
158
|
+
while (this.queue.length > 0 && this.canMakeRequest()) {
|
|
159
|
+
const request = this.queue.shift();
|
|
160
|
+
if (request) {
|
|
161
|
+
request.resolve(undefined);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Set rate limit strategy
|
|
167
|
+
*/
|
|
168
|
+
setStrategy(strategy) {
|
|
169
|
+
this.strategy = strategy;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.RateLimiter = RateLimiter;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core HTTP request handler
|
|
3
|
+
*/
|
|
4
|
+
import type { RateLimiter } from "./rate-limiter.js";
|
|
5
|
+
import type { RequestDeduplicator } from "./deduplication.js";
|
|
6
|
+
import type { RequestConfig, RetryConfig } from "../types/common.js";
|
|
7
|
+
interface RequestOptions {
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
accessToken?: string;
|
|
11
|
+
rateLimiter?: RateLimiter;
|
|
12
|
+
deduplicator?: RequestDeduplicator;
|
|
13
|
+
retryConfig?: RetryConfig;
|
|
14
|
+
normalizeKeys?: boolean;
|
|
15
|
+
transformDates?: boolean;
|
|
16
|
+
flattenResponses?: boolean;
|
|
17
|
+
addComputedFields?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Make HTTP request with retry, rate limiting, and error handling
|
|
21
|
+
*/
|
|
22
|
+
export declare function request<T>(config: RequestConfig, options?: RequestOptions): Promise<T>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core HTTP request handler
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.request = request;
|
|
7
|
+
const errors_js_1 = require("../errors.js");
|
|
8
|
+
const retry_js_1 = require("./retry.js");
|
|
9
|
+
const transformers_js_1 = require("../utils/transformers.js");
|
|
10
|
+
const DEFAULT_BASE_URL = "https://www.strava.com/api/v3";
|
|
11
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
12
|
+
/**
|
|
13
|
+
* Extract rate limit info from response headers
|
|
14
|
+
*/
|
|
15
|
+
function extractRateLimitInfo(headers) {
|
|
16
|
+
const shortTermLimit = headers.get("X-RateLimit-Limit");
|
|
17
|
+
const shortTermUsage = headers.get("X-RateLimit-Usage");
|
|
18
|
+
const dailyLimit = headers.get("X-RateLimit-Limit-Daily");
|
|
19
|
+
const dailyUsage = headers.get("X-RateLimit-Usage-Daily");
|
|
20
|
+
const info = {};
|
|
21
|
+
if (shortTermLimit) {
|
|
22
|
+
info.shortTermLimit = parseInt(shortTermLimit, 10);
|
|
23
|
+
}
|
|
24
|
+
if (shortTermUsage) {
|
|
25
|
+
const parts = shortTermUsage.split(",");
|
|
26
|
+
if (parts.length >= 1) {
|
|
27
|
+
info.shortTermUsage = parseInt(parts[0], 10);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (dailyLimit) {
|
|
31
|
+
info.dailyLimit = parseInt(dailyLimit, 10);
|
|
32
|
+
}
|
|
33
|
+
if (dailyUsage) {
|
|
34
|
+
const parts = dailyUsage.split(",");
|
|
35
|
+
if (parts.length >= 1) {
|
|
36
|
+
info.dailyUsage = parseInt(parts[0], 10);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return info;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build query string from query parameters
|
|
43
|
+
*/
|
|
44
|
+
function buildQueryString(query) {
|
|
45
|
+
if (!query)
|
|
46
|
+
return "";
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
for (const [key, value] of Object.entries(query)) {
|
|
49
|
+
if (value !== undefined && value !== null) {
|
|
50
|
+
params.append(key, String(value));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const queryString = params.toString();
|
|
54
|
+
return queryString ? `?${queryString}` : "";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create error from response
|
|
58
|
+
*/
|
|
59
|
+
async function createErrorFromResponse(response) {
|
|
60
|
+
const statusCode = response.status;
|
|
61
|
+
let errorData;
|
|
62
|
+
try {
|
|
63
|
+
errorData = await response.json();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
errorData = await response.text().catch(() => undefined);
|
|
67
|
+
}
|
|
68
|
+
const message = typeof errorData === "object" && errorData !== null && "message" in errorData
|
|
69
|
+
? String(errorData.message)
|
|
70
|
+
: `HTTP ${statusCode}: ${response.statusText}`;
|
|
71
|
+
const rateLimitInfo = extractRateLimitInfo(response.headers);
|
|
72
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
73
|
+
switch (statusCode) {
|
|
74
|
+
case 401:
|
|
75
|
+
case 403:
|
|
76
|
+
return new errors_js_1.StravaAuthError(message, statusCode, errorData);
|
|
77
|
+
case 404:
|
|
78
|
+
return new errors_js_1.StravaNotFoundError(message, statusCode, errorData);
|
|
79
|
+
case 422: {
|
|
80
|
+
const errors = typeof errorData === "object" && errorData !== null && "errors" in errorData
|
|
81
|
+
? errorData.errors
|
|
82
|
+
: undefined;
|
|
83
|
+
return new errors_js_1.StravaValidationError(message, statusCode, errorData, errors);
|
|
84
|
+
}
|
|
85
|
+
case 429:
|
|
86
|
+
return new errors_js_1.StravaRateLimitError(message, statusCode, errorData, retryAfter ? parseInt(retryAfter, 10) : undefined, rateLimitInfo.shortTermLimit, rateLimitInfo.shortTermUsage);
|
|
87
|
+
case 500:
|
|
88
|
+
case 502:
|
|
89
|
+
case 503:
|
|
90
|
+
case 504:
|
|
91
|
+
return new errors_js_1.StravaServerError(message, statusCode, errorData);
|
|
92
|
+
default:
|
|
93
|
+
return new errors_js_1.StravaError(message, statusCode, errorData);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Make HTTP request with retry, rate limiting, and error handling
|
|
98
|
+
*/
|
|
99
|
+
function request(config, options = {}) {
|
|
100
|
+
const { baseUrl = DEFAULT_BASE_URL, timeout = DEFAULT_TIMEOUT, accessToken, rateLimiter, deduplicator, retryConfig, normalizeKeys = true, transformDates = false, flattenResponses = false, addComputedFields = false, } = options;
|
|
101
|
+
const queryString = buildQueryString(config.query);
|
|
102
|
+
const url = `${baseUrl}${config.path}${queryString}`;
|
|
103
|
+
const headers = {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
...config.headers,
|
|
106
|
+
};
|
|
107
|
+
if (accessToken) {
|
|
108
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
109
|
+
}
|
|
110
|
+
const makeRequest = async () => {
|
|
111
|
+
if (rateLimiter) {
|
|
112
|
+
await rateLimiter.waitForAvailability();
|
|
113
|
+
}
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method: config.method,
|
|
119
|
+
headers,
|
|
120
|
+
body: config.body ? JSON.stringify(config.body) : undefined,
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
});
|
|
123
|
+
if (rateLimiter) {
|
|
124
|
+
rateLimiter.updateFromHeaders(response.headers);
|
|
125
|
+
rateLimiter.processQueue();
|
|
126
|
+
}
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw await createErrorFromResponse(response);
|
|
129
|
+
}
|
|
130
|
+
const rawData = await response.json();
|
|
131
|
+
const transformedData = (0, transformers_js_1.applyTransformations)(rawData, normalizeKeys, transformDates, flattenResponses, addComputedFields);
|
|
132
|
+
return transformedData;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof errors_js_1.StravaError) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
139
|
+
throw new errors_js_1.StravaError(`Request timeout after ${timeout}ms`, undefined, error);
|
|
140
|
+
}
|
|
141
|
+
throw new errors_js_1.StravaError(error instanceof Error ? error.message : "Unknown error", undefined, error);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
clearTimeout(timeoutId);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
if (deduplicator) {
|
|
148
|
+
return deduplicator.getOrCreate(config.method, config.path, config.query, config.body, () => (0, retry_js_1.withRetry)(makeRequest, retryConfig, (error) => {
|
|
149
|
+
if (error instanceof errors_js_1.StravaRateLimitError && error.retryAfter) {
|
|
150
|
+
return error.retryAfter;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
return (0, retry_js_1.withRetry)(makeRequest, retryConfig, (error) => {
|
|
156
|
+
if (error instanceof errors_js_1.StravaRateLimitError && error.retryAfter) {
|
|
157
|
+
return error.retryAfter;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry logic with exponential backoff
|
|
3
|
+
*/
|
|
4
|
+
import type { RetryConfig } from "../types/common.js";
|
|
5
|
+
/**
|
|
6
|
+
* Execute a function with retry logic
|
|
7
|
+
*/
|
|
8
|
+
export declare function withRetry<T>(fn: () => Promise<T>, config?: RetryConfig, getRetryAfter?: (error: unknown) => number | undefined): Promise<T>;
|
|
9
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Retry logic with exponential backoff
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.withRetry = withRetry;
|
|
7
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
8
|
+
maxAttempts: 3,
|
|
9
|
+
initialDelay: 1000,
|
|
10
|
+
maxDelay: 10000,
|
|
11
|
+
backoffFactor: 2,
|
|
12
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Calculate delay for retry attempt
|
|
16
|
+
*/
|
|
17
|
+
function calculateDelay(attempt, config, retryAfter) {
|
|
18
|
+
if (retryAfter !== undefined) {
|
|
19
|
+
return Math.min(retryAfter * 1000, config.maxDelay);
|
|
20
|
+
}
|
|
21
|
+
const delay = config.initialDelay * Math.pow(config.backoffFactor, attempt - 1);
|
|
22
|
+
return Math.min(delay, config.maxDelay);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sleep for specified milliseconds
|
|
26
|
+
*/
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if error/status code is retryable
|
|
32
|
+
*/
|
|
33
|
+
function isRetryable(statusCode, config) {
|
|
34
|
+
if (statusCode === undefined) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return config.retryableStatusCodes.includes(statusCode);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Execute a function with retry logic
|
|
41
|
+
*/
|
|
42
|
+
async function withRetry(fn, config, getRetryAfter) {
|
|
43
|
+
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
44
|
+
let lastError;
|
|
45
|
+
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastError = error;
|
|
51
|
+
if (attempt >= retryConfig.maxAttempts) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const statusCode = error && typeof error === "object" && "statusCode" in error ? error.statusCode : undefined;
|
|
55
|
+
if (!isRetryable(statusCode, retryConfig)) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const retryAfter = getRetryAfter ? getRetryAfter(error) : undefined;
|
|
59
|
+
const delay = calculateDelay(attempt, retryConfig, retryAfter);
|
|
60
|
+
await sleep(delay);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw lastError;
|
|
64
|
+
}
|