@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limit tracking and queue management
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Rate limiter for Strava API
|
|
6
|
+
*/
|
|
7
|
+
export class RateLimiter {
|
|
8
|
+
constructor(strategy = "queue") {
|
|
9
|
+
Object.defineProperty(this, "shortTermLimit", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: 600
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "shortTermWindow", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: 15 * 60 * 1000
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "dailyLimit", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: 30000
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "dailyWindow", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: 24 * 60 * 60 * 1000
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(this, "shortTermRequests", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: []
|
|
38
|
+
});
|
|
39
|
+
Object.defineProperty(this, "dailyRequests", {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
writable: true,
|
|
43
|
+
value: []
|
|
44
|
+
});
|
|
45
|
+
Object.defineProperty(this, "queue", {
|
|
46
|
+
enumerable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
writable: true,
|
|
49
|
+
value: []
|
|
50
|
+
});
|
|
51
|
+
Object.defineProperty(this, "strategy", {
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
writable: true,
|
|
55
|
+
value: void 0
|
|
56
|
+
});
|
|
57
|
+
this.strategy = strategy;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Update rate limit info from response headers
|
|
61
|
+
*/
|
|
62
|
+
updateFromHeaders(headers) {
|
|
63
|
+
const shortTermLimit = headers.get("X-RateLimit-Limit");
|
|
64
|
+
const shortTermUsage = headers.get("X-RateLimit-Usage");
|
|
65
|
+
const dailyLimit = headers.get("X-RateLimit-Limit-Daily");
|
|
66
|
+
const dailyUsage = headers.get("X-RateLimit-Usage-Daily");
|
|
67
|
+
if (shortTermLimit) {
|
|
68
|
+
this.shortTermLimit = parseInt(shortTermLimit, 10);
|
|
69
|
+
}
|
|
70
|
+
if (shortTermUsage) {
|
|
71
|
+
const parts = shortTermUsage.split(",");
|
|
72
|
+
if (parts.length >= 1) {
|
|
73
|
+
const current = parseInt(parts[0], 10);
|
|
74
|
+
this.recordRequest("shortTerm", current);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (dailyLimit) {
|
|
78
|
+
this.dailyLimit = parseInt(dailyLimit, 10);
|
|
79
|
+
}
|
|
80
|
+
if (dailyUsage) {
|
|
81
|
+
const parts = dailyUsage.split(",");
|
|
82
|
+
if (parts.length >= 1) {
|
|
83
|
+
const current = parseInt(parts[0], 10);
|
|
84
|
+
this.recordRequest("daily", current);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Record a request timestamp
|
|
90
|
+
*/
|
|
91
|
+
recordRequest(type, _currentUsage) {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const window = type === "shortTerm" ? this.shortTermWindow : this.dailyWindow;
|
|
94
|
+
const requests = type === "shortTerm" ? this.shortTermRequests : this.dailyRequests;
|
|
95
|
+
const cutoff = now - window;
|
|
96
|
+
while (requests.length > 0 && requests[0] < cutoff) {
|
|
97
|
+
requests.shift();
|
|
98
|
+
}
|
|
99
|
+
requests.push(now);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get current rate limit info
|
|
103
|
+
*/
|
|
104
|
+
getRateLimitInfo() {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const shortTermCutoff = now - this.shortTermWindow;
|
|
107
|
+
this.shortTermRequests = this.shortTermRequests.filter((t) => t >= shortTermCutoff);
|
|
108
|
+
const dailyCutoff = now - this.dailyWindow;
|
|
109
|
+
this.dailyRequests = this.dailyRequests.filter((t) => t >= dailyCutoff);
|
|
110
|
+
return {
|
|
111
|
+
shortTermLimit: this.shortTermLimit,
|
|
112
|
+
shortTermUsage: this.shortTermRequests.length,
|
|
113
|
+
dailyLimit: this.dailyLimit,
|
|
114
|
+
dailyUsage: this.dailyRequests.length,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check if we can make a request
|
|
119
|
+
*/
|
|
120
|
+
canMakeRequest() {
|
|
121
|
+
const info = this.getRateLimitInfo();
|
|
122
|
+
return (info.shortTermUsage ?? 0) < (info.shortTermLimit ?? 600) &&
|
|
123
|
+
(info.dailyUsage ?? 0) < (info.dailyLimit ?? 30000);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Wait until we can make a request
|
|
127
|
+
*/
|
|
128
|
+
async waitForAvailability() {
|
|
129
|
+
if (this.canMakeRequest()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this.strategy === "throw") {
|
|
133
|
+
const info = this.getRateLimitInfo();
|
|
134
|
+
throw new Error(`Rate limit exceeded. Short-term: ${info.shortTermUsage}/${info.shortTermLimit}, ` +
|
|
135
|
+
`Daily: ${info.dailyUsage}/${info.dailyLimit}`);
|
|
136
|
+
}
|
|
137
|
+
if (this.strategy === "wait") {
|
|
138
|
+
while (!this.canMakeRequest()) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
this.queue.push({
|
|
145
|
+
resolve,
|
|
146
|
+
reject,
|
|
147
|
+
timestamp: Date.now(),
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Process queued requests
|
|
153
|
+
*/
|
|
154
|
+
processQueue() {
|
|
155
|
+
while (this.queue.length > 0 && this.canMakeRequest()) {
|
|
156
|
+
const request = this.queue.shift();
|
|
157
|
+
if (request) {
|
|
158
|
+
request.resolve(undefined);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Set rate limit strategy
|
|
164
|
+
*/
|
|
165
|
+
setStrategy(strategy) {
|
|
166
|
+
this.strategy = strategy;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -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,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core HTTP request handler
|
|
3
|
+
*/
|
|
4
|
+
import { StravaAuthError, StravaError, StravaNotFoundError, StravaRateLimitError, StravaServerError, StravaValidationError } from "../errors.js";
|
|
5
|
+
import { withRetry } from "./retry.js";
|
|
6
|
+
import { applyTransformations } from "../utils/transformers.js";
|
|
7
|
+
const DEFAULT_BASE_URL = "https://www.strava.com/api/v3";
|
|
8
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
9
|
+
/**
|
|
10
|
+
* Extract rate limit info from response headers
|
|
11
|
+
*/
|
|
12
|
+
function extractRateLimitInfo(headers) {
|
|
13
|
+
const shortTermLimit = headers.get("X-RateLimit-Limit");
|
|
14
|
+
const shortTermUsage = headers.get("X-RateLimit-Usage");
|
|
15
|
+
const dailyLimit = headers.get("X-RateLimit-Limit-Daily");
|
|
16
|
+
const dailyUsage = headers.get("X-RateLimit-Usage-Daily");
|
|
17
|
+
const info = {};
|
|
18
|
+
if (shortTermLimit) {
|
|
19
|
+
info.shortTermLimit = parseInt(shortTermLimit, 10);
|
|
20
|
+
}
|
|
21
|
+
if (shortTermUsage) {
|
|
22
|
+
const parts = shortTermUsage.split(",");
|
|
23
|
+
if (parts.length >= 1) {
|
|
24
|
+
info.shortTermUsage = parseInt(parts[0], 10);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (dailyLimit) {
|
|
28
|
+
info.dailyLimit = parseInt(dailyLimit, 10);
|
|
29
|
+
}
|
|
30
|
+
if (dailyUsage) {
|
|
31
|
+
const parts = dailyUsage.split(",");
|
|
32
|
+
if (parts.length >= 1) {
|
|
33
|
+
info.dailyUsage = parseInt(parts[0], 10);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return info;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build query string from query parameters
|
|
40
|
+
*/
|
|
41
|
+
function buildQueryString(query) {
|
|
42
|
+
if (!query)
|
|
43
|
+
return "";
|
|
44
|
+
const params = new URLSearchParams();
|
|
45
|
+
for (const [key, value] of Object.entries(query)) {
|
|
46
|
+
if (value !== undefined && value !== null) {
|
|
47
|
+
params.append(key, String(value));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const queryString = params.toString();
|
|
51
|
+
return queryString ? `?${queryString}` : "";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create error from response
|
|
55
|
+
*/
|
|
56
|
+
async function createErrorFromResponse(response) {
|
|
57
|
+
const statusCode = response.status;
|
|
58
|
+
let errorData;
|
|
59
|
+
try {
|
|
60
|
+
errorData = await response.json();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
errorData = await response.text().catch(() => undefined);
|
|
64
|
+
}
|
|
65
|
+
const message = typeof errorData === "object" && errorData !== null && "message" in errorData
|
|
66
|
+
? String(errorData.message)
|
|
67
|
+
: `HTTP ${statusCode}: ${response.statusText}`;
|
|
68
|
+
const rateLimitInfo = extractRateLimitInfo(response.headers);
|
|
69
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
70
|
+
switch (statusCode) {
|
|
71
|
+
case 401:
|
|
72
|
+
case 403:
|
|
73
|
+
return new StravaAuthError(message, statusCode, errorData);
|
|
74
|
+
case 404:
|
|
75
|
+
return new StravaNotFoundError(message, statusCode, errorData);
|
|
76
|
+
case 422: {
|
|
77
|
+
const errors = typeof errorData === "object" && errorData !== null && "errors" in errorData
|
|
78
|
+
? errorData.errors
|
|
79
|
+
: undefined;
|
|
80
|
+
return new StravaValidationError(message, statusCode, errorData, errors);
|
|
81
|
+
}
|
|
82
|
+
case 429:
|
|
83
|
+
return new StravaRateLimitError(message, statusCode, errorData, retryAfter ? parseInt(retryAfter, 10) : undefined, rateLimitInfo.shortTermLimit, rateLimitInfo.shortTermUsage);
|
|
84
|
+
case 500:
|
|
85
|
+
case 502:
|
|
86
|
+
case 503:
|
|
87
|
+
case 504:
|
|
88
|
+
return new StravaServerError(message, statusCode, errorData);
|
|
89
|
+
default:
|
|
90
|
+
return new StravaError(message, statusCode, errorData);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Make HTTP request with retry, rate limiting, and error handling
|
|
95
|
+
*/
|
|
96
|
+
export function request(config, options = {}) {
|
|
97
|
+
const { baseUrl = DEFAULT_BASE_URL, timeout = DEFAULT_TIMEOUT, accessToken, rateLimiter, deduplicator, retryConfig, normalizeKeys = true, transformDates = false, flattenResponses = false, addComputedFields = false, } = options;
|
|
98
|
+
const queryString = buildQueryString(config.query);
|
|
99
|
+
const url = `${baseUrl}${config.path}${queryString}`;
|
|
100
|
+
const headers = {
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
...config.headers,
|
|
103
|
+
};
|
|
104
|
+
if (accessToken) {
|
|
105
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
106
|
+
}
|
|
107
|
+
const makeRequest = async () => {
|
|
108
|
+
if (rateLimiter) {
|
|
109
|
+
await rateLimiter.waitForAvailability();
|
|
110
|
+
}
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(url, {
|
|
115
|
+
method: config.method,
|
|
116
|
+
headers,
|
|
117
|
+
body: config.body ? JSON.stringify(config.body) : undefined,
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
});
|
|
120
|
+
if (rateLimiter) {
|
|
121
|
+
rateLimiter.updateFromHeaders(response.headers);
|
|
122
|
+
rateLimiter.processQueue();
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw await createErrorFromResponse(response);
|
|
126
|
+
}
|
|
127
|
+
const rawData = await response.json();
|
|
128
|
+
const transformedData = applyTransformations(rawData, normalizeKeys, transformDates, flattenResponses, addComputedFields);
|
|
129
|
+
return transformedData;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof StravaError) {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
136
|
+
throw new StravaError(`Request timeout after ${timeout}ms`, undefined, error);
|
|
137
|
+
}
|
|
138
|
+
throw new StravaError(error instanceof Error ? error.message : "Unknown error", undefined, error);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
if (deduplicator) {
|
|
145
|
+
return deduplicator.getOrCreate(config.method, config.path, config.query, config.body, () => withRetry(makeRequest, retryConfig, (error) => {
|
|
146
|
+
if (error instanceof StravaRateLimitError && error.retryAfter) {
|
|
147
|
+
return error.retryAfter;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
return withRetry(makeRequest, retryConfig, (error) => {
|
|
153
|
+
if (error instanceof StravaRateLimitError && error.retryAfter) {
|
|
154
|
+
return error.retryAfter;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry logic with exponential backoff
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
5
|
+
maxAttempts: 3,
|
|
6
|
+
initialDelay: 1000,
|
|
7
|
+
maxDelay: 10000,
|
|
8
|
+
backoffFactor: 2,
|
|
9
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Calculate delay for retry attempt
|
|
13
|
+
*/
|
|
14
|
+
function calculateDelay(attempt, config, retryAfter) {
|
|
15
|
+
if (retryAfter !== undefined) {
|
|
16
|
+
return Math.min(retryAfter * 1000, config.maxDelay);
|
|
17
|
+
}
|
|
18
|
+
const delay = config.initialDelay * Math.pow(config.backoffFactor, attempt - 1);
|
|
19
|
+
return Math.min(delay, config.maxDelay);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sleep for specified milliseconds
|
|
23
|
+
*/
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if error/status code is retryable
|
|
29
|
+
*/
|
|
30
|
+
function isRetryable(statusCode, config) {
|
|
31
|
+
if (statusCode === undefined) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return config.retryableStatusCodes.includes(statusCode);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Execute a function with retry logic
|
|
38
|
+
*/
|
|
39
|
+
export async function withRetry(fn, config, getRetryAfter) {
|
|
40
|
+
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
41
|
+
let lastError;
|
|
42
|
+
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
|
|
43
|
+
try {
|
|
44
|
+
return await fn();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
lastError = error;
|
|
48
|
+
if (attempt >= retryConfig.maxAttempts) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
const statusCode = error && typeof error === "object" && "statusCode" in error ? error.statusCode : undefined;
|
|
52
|
+
if (!isRetryable(statusCode, retryConfig)) {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const retryAfter = getRetryAfter ? getRetryAfter(error) : undefined;
|
|
56
|
+
const delay = calculateDelay(attempt, retryConfig, retryAfter);
|
|
57
|
+
await sleep(delay);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw lastError;
|
|
61
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activities resource
|
|
3
|
+
*/
|
|
4
|
+
import type { StravaClient } from "../client.js";
|
|
5
|
+
import type { ActivityZone, Comment, DetailedActivity, DetailedSegmentEffort, Lap, StreamSet, SummaryActivity, SummaryAthlete } from "../types/api.js";
|
|
6
|
+
import type { StreamType } from "./streams.js";
|
|
7
|
+
/**
|
|
8
|
+
* Options for listing activities
|
|
9
|
+
*/
|
|
10
|
+
export interface ListActivitiesOptions {
|
|
11
|
+
/** Unix timestamp for activities before this time */
|
|
12
|
+
before?: number;
|
|
13
|
+
/** Unix timestamp for activities after this time */
|
|
14
|
+
after?: number;
|
|
15
|
+
/** Page number (default: 1) */
|
|
16
|
+
page?: number;
|
|
17
|
+
/** Number of items per page (default: 30, max: 200) */
|
|
18
|
+
perPage?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Data for creating a new activity
|
|
22
|
+
*/
|
|
23
|
+
export interface CreateActivityData {
|
|
24
|
+
/** Activity name */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Activity type (e.g., "Run", "Ride") */
|
|
27
|
+
type: string;
|
|
28
|
+
/** Start date in ISO 8601 format */
|
|
29
|
+
startDateLocal: string;
|
|
30
|
+
/** Elapsed time in seconds */
|
|
31
|
+
elapsedTime: number;
|
|
32
|
+
/** Activity description */
|
|
33
|
+
description?: string;
|
|
34
|
+
/** Distance in meters */
|
|
35
|
+
distance?: number;
|
|
36
|
+
/** Whether activity was done on trainer */
|
|
37
|
+
trainer?: boolean;
|
|
38
|
+
/** Whether activity is a commute */
|
|
39
|
+
commute?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Data for updating an existing activity
|
|
43
|
+
*/
|
|
44
|
+
export interface UpdateActivityData {
|
|
45
|
+
/** Activity name */
|
|
46
|
+
name?: string;
|
|
47
|
+
/** Activity type (e.g., "Run", "Ride") */
|
|
48
|
+
type?: string;
|
|
49
|
+
/** Sport type */
|
|
50
|
+
sportType?: string;
|
|
51
|
+
/** Start date in ISO 8601 format */
|
|
52
|
+
startDateLocal?: string;
|
|
53
|
+
/** Elapsed time in seconds */
|
|
54
|
+
elapsedTime?: number;
|
|
55
|
+
/** Activity description */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Distance in meters */
|
|
58
|
+
distance?: number;
|
|
59
|
+
/** Whether activity was done on trainer */
|
|
60
|
+
trainer?: boolean;
|
|
61
|
+
/** Whether activity is a commute */
|
|
62
|
+
commute?: boolean;
|
|
63
|
+
/** Gear ID */
|
|
64
|
+
gearId?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resource for interacting with Strava activities
|
|
68
|
+
*/
|
|
69
|
+
export declare class ActivitiesResource {
|
|
70
|
+
private client;
|
|
71
|
+
constructor(client: StravaClient);
|
|
72
|
+
/**
|
|
73
|
+
* Get activity by ID
|
|
74
|
+
*/
|
|
75
|
+
getById(id: number): Promise<DetailedActivity>;
|
|
76
|
+
/**
|
|
77
|
+
* List athlete activities
|
|
78
|
+
*/
|
|
79
|
+
list(options?: ListActivitiesOptions): Promise<SummaryActivity[]>;
|
|
80
|
+
/**
|
|
81
|
+
* List all activities (auto-pagination)
|
|
82
|
+
*/
|
|
83
|
+
listAll(options?: Omit<ListActivitiesOptions, "page">): AsyncGenerator<SummaryActivity, void, unknown>;
|
|
84
|
+
/**
|
|
85
|
+
* Create activity
|
|
86
|
+
*/
|
|
87
|
+
create(data: CreateActivityData): Promise<DetailedActivity>;
|
|
88
|
+
/**
|
|
89
|
+
* Update activity
|
|
90
|
+
*/
|
|
91
|
+
update(id: number, data: UpdateActivityData): Promise<DetailedActivity>;
|
|
92
|
+
/**
|
|
93
|
+
* Delete activity
|
|
94
|
+
*/
|
|
95
|
+
delete(id: number): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Get activity with details (combines activity + laps + zones + comments + kudoers)
|
|
98
|
+
*/
|
|
99
|
+
getWithDetails(id: number): Promise<DetailedActivity & {
|
|
100
|
+
laps: Lap[];
|
|
101
|
+
zones: ActivityZone[];
|
|
102
|
+
comments: Comment[];
|
|
103
|
+
kudoers: SummaryAthlete[];
|
|
104
|
+
}>;
|
|
105
|
+
/**
|
|
106
|
+
* Get activity laps
|
|
107
|
+
*/
|
|
108
|
+
getLaps(id: number): Promise<Lap[]>;
|
|
109
|
+
/**
|
|
110
|
+
* Get activity zones
|
|
111
|
+
*/
|
|
112
|
+
getZones(id: number): Promise<ActivityZone[]>;
|
|
113
|
+
/**
|
|
114
|
+
* Get activity comments
|
|
115
|
+
*/
|
|
116
|
+
getComments(id: number, options?: {
|
|
117
|
+
page?: number;
|
|
118
|
+
perPage?: number;
|
|
119
|
+
}): Promise<Comment[]>;
|
|
120
|
+
/**
|
|
121
|
+
* Get activity kudoers
|
|
122
|
+
*/
|
|
123
|
+
getKudoers(id: number, options?: {
|
|
124
|
+
page?: number;
|
|
125
|
+
perPage?: number;
|
|
126
|
+
}): Promise<SummaryAthlete[]>;
|
|
127
|
+
/**
|
|
128
|
+
* Analyze activity with comprehensive data
|
|
129
|
+
* Combines activity, zones, laps, best efforts (segments), and optionally streams
|
|
130
|
+
*/
|
|
131
|
+
analyze(id: number, options?: {
|
|
132
|
+
includeStreams?: boolean;
|
|
133
|
+
streamTypes?: StreamType[];
|
|
134
|
+
}): Promise<DetailedActivity & {
|
|
135
|
+
zones: ActivityZone[];
|
|
136
|
+
laps: Lap[];
|
|
137
|
+
bestEfforts: DetailedSegmentEffort[];
|
|
138
|
+
streams?: StreamSet;
|
|
139
|
+
analysis: {
|
|
140
|
+
hasPowerData: boolean;
|
|
141
|
+
hasHeartRateData: boolean;
|
|
142
|
+
totalLaps: number;
|
|
143
|
+
bestEffortCount: number;
|
|
144
|
+
averageLapTime?: number;
|
|
145
|
+
averageLapDistance?: number;
|
|
146
|
+
};
|
|
147
|
+
}>;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=activities.d.ts.map
|