irdata_js 0.1.0 → 0.2.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/README.md +38 -4
- package/dist/client.d.ts +52 -1
- package/dist/client.js +138 -34
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -54,18 +54,52 @@ if (code) {
|
|
|
54
54
|
|
|
55
55
|
### 3. Fetch Data
|
|
56
56
|
|
|
57
|
-
Once authenticated, you can call any endpoint using `getData`. This method handles authentication headers
|
|
57
|
+
Once authenticated, you can call any endpoint using `getData`. This method handles authentication headers, automatically follows S3 links if returned by the API, and provides metadata about the response.
|
|
58
58
|
|
|
59
59
|
```javascript
|
|
60
60
|
try {
|
|
61
61
|
// Call an endpoint directly
|
|
62
|
-
const
|
|
63
|
-
|
|
62
|
+
const { data, metadata } = await client.getData('/member/info');
|
|
63
|
+
|
|
64
|
+
console.log(data); // The actual API response
|
|
65
|
+
console.log(metadata.sizeBytes); // Response size in bytes
|
|
66
|
+
console.log(metadata.chunksDetected); // Boolean indicating if data is chunked
|
|
64
67
|
} catch (error) {
|
|
65
68
|
console.error('Failed to fetch member info:', error);
|
|
66
69
|
}
|
|
67
70
|
```
|
|
68
71
|
|
|
72
|
+
### 4. Handling Large Datasets (Chunks)
|
|
73
|
+
|
|
74
|
+
Some iRacing endpoints (like large result sets) return data in multiple "chunks" hosted on S3. When `metadata.chunksDetected` is true, you can use the library to fetch the rest of the data.
|
|
75
|
+
|
|
76
|
+
#### Fetch all chunks at once
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
const result = await client.getData('/results/get');
|
|
80
|
+
|
|
81
|
+
if (result.metadata.chunksDetected) {
|
|
82
|
+
// Fetch and merge all chunks into a single array
|
|
83
|
+
const { data: allResults } = await client.getChunks(result.data);
|
|
84
|
+
console.log('Total results:', allResults.length);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Fetch chunks individually (Pagination)
|
|
89
|
+
|
|
90
|
+
For extremely large datasets, you might want to fetch chunks one by one:
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
if (result.metadata.chunksDetected) {
|
|
94
|
+
const totalChunks = result.data.chunk_info.chunk_file_names.length;
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
97
|
+
const { data: chunk } = await client.getChunk(result.data, i);
|
|
98
|
+
console.log(`Processing chunk ${i + 1}/${totalChunks}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
69
103
|
## Development
|
|
70
104
|
|
|
71
105
|
### Build
|
|
@@ -103,4 +137,4 @@ This repository includes a local development proxy server to test the OAuth flow
|
|
|
103
137
|
|
|
104
138
|
## License
|
|
105
139
|
|
|
106
|
-
|
|
140
|
+
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -9,20 +9,71 @@ interface ClientConfig {
|
|
|
9
9
|
tokenEndpoint?: string;
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
export interface ChunkInfo {
|
|
13
|
+
chunk_size: number;
|
|
14
|
+
num_chunks: number;
|
|
15
|
+
rows: number;
|
|
16
|
+
base_download_url: string;
|
|
17
|
+
chunk_file_names: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface DataResult<T> {
|
|
20
|
+
data: T;
|
|
21
|
+
metadata: {
|
|
22
|
+
s3LinkFollowed: boolean;
|
|
23
|
+
chunksDetected: boolean;
|
|
24
|
+
sizeBytes: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface ChunkResult<T> {
|
|
28
|
+
data: T;
|
|
29
|
+
metadata: {
|
|
30
|
+
sizeBytes: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
12
33
|
export declare class IRacingClient {
|
|
13
34
|
auth: AuthManager;
|
|
14
35
|
private apiUrl;
|
|
15
36
|
private fileProxyUrl?;
|
|
16
37
|
constructor(config?: ClientConfig);
|
|
38
|
+
private calculateSize;
|
|
39
|
+
/**
|
|
40
|
+
* Internal method to perform request and return data + size.
|
|
41
|
+
*/
|
|
42
|
+
private requestInternal;
|
|
17
43
|
/**
|
|
18
44
|
* Performs a fetch request with authentication headers.
|
|
19
45
|
* Does NOT automatically follow "link" responses.
|
|
20
46
|
*/
|
|
21
47
|
request<T>(endpoint: string, options?: RequestInit): Promise<T>;
|
|
22
48
|
private handleErrorResponse;
|
|
49
|
+
/**
|
|
50
|
+
* Helper to fetch data from an external URL (e.g. S3), handling the file proxy if configured.
|
|
51
|
+
*/
|
|
52
|
+
private fetchExternal;
|
|
23
53
|
/**
|
|
24
54
|
* Fetches data from an endpoint, automatically following any S3 links returned.
|
|
55
|
+
* Returns metadata about the operation.
|
|
56
|
+
*/
|
|
57
|
+
getData<T>(endpoint: string): Promise<DataResult<T>>;
|
|
58
|
+
private hasChunks;
|
|
59
|
+
/**
|
|
60
|
+
* Fetches a specific chunk from a response containing chunk_info.
|
|
61
|
+
*
|
|
62
|
+
* @param data The response object containing chunk_info
|
|
63
|
+
* @param chunkIndex The index of the chunk to fetch (0-based)
|
|
64
|
+
* @returns The content of the chunk and metadata
|
|
65
|
+
*/
|
|
66
|
+
getChunk<T>(data: unknown, chunkIndex: number): Promise<ChunkResult<T[]>>;
|
|
67
|
+
/**
|
|
68
|
+
* Fetches multiple chunks and merges the results into a single array.
|
|
69
|
+
*
|
|
70
|
+
* @param data The response object containing chunk_info
|
|
71
|
+
* @param options Options for fetching chunks (start index, limit count)
|
|
72
|
+
* @returns A merged array of data from the requested chunks and total size
|
|
25
73
|
*/
|
|
26
|
-
|
|
74
|
+
getChunks<T>(data: unknown, options?: {
|
|
75
|
+
start?: number;
|
|
76
|
+
limit?: number;
|
|
77
|
+
}): Promise<ChunkResult<T[]>>;
|
|
27
78
|
}
|
|
28
79
|
export {};
|
package/dist/client.js
CHANGED
|
@@ -9,12 +9,22 @@ export class IRacingClient {
|
|
|
9
9
|
this.fileProxyUrl = config.fileProxyUrl;
|
|
10
10
|
this.auth = new AuthManager(config.auth);
|
|
11
11
|
}
|
|
12
|
+
calculateSize(response, data) {
|
|
13
|
+
const cl = response.headers.get('content-length');
|
|
14
|
+
if (cl)
|
|
15
|
+
return parseInt(cl, 10);
|
|
16
|
+
try {
|
|
17
|
+
return new TextEncoder().encode(JSON.stringify(data)).length;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
/**
|
|
13
|
-
*
|
|
14
|
-
* Does NOT automatically follow "link" responses.
|
|
24
|
+
* Internal method to perform request and return data + size.
|
|
15
25
|
*/
|
|
16
|
-
async
|
|
17
|
-
// Remove leading slash from endpoint if present
|
|
26
|
+
async requestInternal(endpoint, options = {}) {
|
|
27
|
+
// Remove leading slash from endpoint if present
|
|
18
28
|
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
|
19
29
|
const cleanApiUrl = this.apiUrl.endsWith('/') ? this.apiUrl : `${this.apiUrl}/`;
|
|
20
30
|
const url = `${cleanApiUrl}${cleanEndpoint}`;
|
|
@@ -27,7 +37,7 @@ export class IRacingClient {
|
|
|
27
37
|
'Content-Type': 'application/json',
|
|
28
38
|
},
|
|
29
39
|
};
|
|
30
|
-
|
|
40
|
+
let response = await fetch(url, mergedOptions);
|
|
31
41
|
if (!response.ok) {
|
|
32
42
|
if (response.status === 401) {
|
|
33
43
|
// Try to refresh token
|
|
@@ -44,22 +54,28 @@ export class IRacingClient {
|
|
|
44
54
|
'Content-Type': 'application/json',
|
|
45
55
|
},
|
|
46
56
|
};
|
|
47
|
-
|
|
48
|
-
if (retryResponse.ok) {
|
|
49
|
-
return retryResponse.json();
|
|
50
|
-
}
|
|
51
|
-
// If retry fails, use the retryResponse for error handling below
|
|
52
|
-
return this.handleErrorResponse(retryResponse);
|
|
57
|
+
response = await fetch(url, retryOptions);
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
catch (refreshError) {
|
|
56
61
|
console.error('Token refresh failed during request retry:', refreshError);
|
|
57
|
-
// Fall through to original 401 error handling
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
return this.handleErrorResponse(response);
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
|
-
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
const sizeBytes = this.calculateSize(response, data);
|
|
70
|
+
return { data, sizeBytes };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Performs a fetch request with authentication headers.
|
|
74
|
+
* Does NOT automatically follow "link" responses.
|
|
75
|
+
*/
|
|
76
|
+
async request(endpoint, options = {}) {
|
|
77
|
+
const { data } = await this.requestInternal(endpoint, options);
|
|
78
|
+
return data;
|
|
63
79
|
}
|
|
64
80
|
async handleErrorResponse(response) {
|
|
65
81
|
let body;
|
|
@@ -77,33 +93,121 @@ export class IRacingClient {
|
|
|
77
93
|
}
|
|
78
94
|
throw new IRacingAPIError(`API Request failed: ${response.status} ${response.statusText}`, response.status, response.statusText, body);
|
|
79
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Helper to fetch data from an external URL (e.g. S3), handling the file proxy if configured.
|
|
98
|
+
*/
|
|
99
|
+
async fetchExternal(url) {
|
|
100
|
+
let fetchUrl = url;
|
|
101
|
+
if (this.fileProxyUrl) {
|
|
102
|
+
// If a file proxy is configured, use it
|
|
103
|
+
const separator = this.fileProxyUrl.includes('?') ? '&' : '?';
|
|
104
|
+
fetchUrl = `${this.fileProxyUrl}${separator}url=${encodeURIComponent(url)}`;
|
|
105
|
+
}
|
|
106
|
+
const response = await fetch(fetchUrl);
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
return this.handleErrorResponse(response);
|
|
109
|
+
}
|
|
110
|
+
const data = await response.json();
|
|
111
|
+
const sizeBytes = this.calculateSize(response, data);
|
|
112
|
+
return { data, sizeBytes };
|
|
113
|
+
}
|
|
80
114
|
/**
|
|
81
115
|
* Fetches data from an endpoint, automatically following any S3 links returned.
|
|
116
|
+
* Returns metadata about the operation.
|
|
82
117
|
*/
|
|
83
118
|
async getData(endpoint) {
|
|
84
|
-
const data = await this.
|
|
119
|
+
const { data: initialData, sizeBytes: initialSize } = await this.requestInternal(endpoint);
|
|
85
120
|
// Check if the response contains a generic link to S3 and follow it
|
|
86
|
-
if (
|
|
87
|
-
typeof
|
|
88
|
-
'link' in
|
|
89
|
-
typeof
|
|
90
|
-
const s3Link =
|
|
121
|
+
if (initialData &&
|
|
122
|
+
typeof initialData === 'object' &&
|
|
123
|
+
'link' in initialData &&
|
|
124
|
+
typeof initialData.link === 'string') {
|
|
125
|
+
const s3Link = initialData.link;
|
|
91
126
|
if (s3Link.startsWith('http')) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!linkResponse.ok) {
|
|
102
|
-
return this.handleErrorResponse(linkResponse);
|
|
103
|
-
}
|
|
104
|
-
return linkResponse.json();
|
|
127
|
+
const { data: externalData, sizeBytes: externalSize } = await this.fetchExternal(s3Link);
|
|
128
|
+
return {
|
|
129
|
+
data: externalData,
|
|
130
|
+
metadata: {
|
|
131
|
+
s3LinkFollowed: true,
|
|
132
|
+
chunksDetected: this.hasChunks(externalData),
|
|
133
|
+
sizeBytes: externalSize,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
105
136
|
}
|
|
106
137
|
}
|
|
107
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
data: initialData,
|
|
140
|
+
metadata: {
|
|
141
|
+
s3LinkFollowed: false,
|
|
142
|
+
chunksDetected: this.hasChunks(initialData),
|
|
143
|
+
sizeBytes: initialSize,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
hasChunks(data) {
|
|
148
|
+
return !!(data && typeof data === 'object' && 'chunk_info' in data);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Fetches a specific chunk from a response containing chunk_info.
|
|
152
|
+
*
|
|
153
|
+
* @param data The response object containing chunk_info
|
|
154
|
+
* @param chunkIndex The index of the chunk to fetch (0-based)
|
|
155
|
+
* @returns The content of the chunk and metadata
|
|
156
|
+
*/
|
|
157
|
+
async getChunk(data, chunkIndex) {
|
|
158
|
+
const dataWithChunks = data;
|
|
159
|
+
if (!dataWithChunks || !dataWithChunks.chunk_info) {
|
|
160
|
+
throw new Error('Response does not contain chunk_info');
|
|
161
|
+
}
|
|
162
|
+
const chunkInfo = dataWithChunks.chunk_info;
|
|
163
|
+
const { base_download_url, chunk_file_names } = chunkInfo;
|
|
164
|
+
if (!base_download_url ||
|
|
165
|
+
!Array.isArray(chunk_file_names) ||
|
|
166
|
+
chunkIndex < 0 ||
|
|
167
|
+
chunkIndex >= chunk_file_names.length) {
|
|
168
|
+
throw new Error(`Invalid chunk index: ${chunkIndex} (Total chunks: ${chunk_file_names?.length || 0})`);
|
|
169
|
+
}
|
|
170
|
+
const chunkUrl = `${base_download_url}${chunk_file_names[chunkIndex]}`;
|
|
171
|
+
const { data: chunkData, sizeBytes } = await this.fetchExternal(chunkUrl);
|
|
172
|
+
return {
|
|
173
|
+
data: chunkData,
|
|
174
|
+
metadata: {
|
|
175
|
+
sizeBytes,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetches multiple chunks and merges the results into a single array.
|
|
181
|
+
*
|
|
182
|
+
* @param data The response object containing chunk_info
|
|
183
|
+
* @param options Options for fetching chunks (start index, limit count)
|
|
184
|
+
* @returns A merged array of data from the requested chunks and total size
|
|
185
|
+
*/
|
|
186
|
+
async getChunks(data, options = {}) {
|
|
187
|
+
const dataWithChunks = data;
|
|
188
|
+
if (!dataWithChunks || !dataWithChunks.chunk_info) {
|
|
189
|
+
throw new Error('Response does not contain chunk_info');
|
|
190
|
+
}
|
|
191
|
+
const chunkInfo = dataWithChunks.chunk_info;
|
|
192
|
+
const totalChunks = chunkInfo.chunk_file_names.length;
|
|
193
|
+
const start = options.start || 0;
|
|
194
|
+
const limit = options.limit || totalChunks - start;
|
|
195
|
+
const end = Math.min(start + limit, totalChunks);
|
|
196
|
+
if (start < 0 || start >= totalChunks) {
|
|
197
|
+
throw new Error(`Invalid start index: ${start} (Total chunks: ${totalChunks})`);
|
|
198
|
+
}
|
|
199
|
+
const chunkPromises = [];
|
|
200
|
+
for (let i = start; i < end; i++) {
|
|
201
|
+
chunkPromises.push(this.getChunk(data, i));
|
|
202
|
+
}
|
|
203
|
+
const results = await Promise.all(chunkPromises);
|
|
204
|
+
const mergedData = results.flatMap((r) => r.data);
|
|
205
|
+
const totalSize = results.reduce((sum, r) => sum + r.metadata.sizeBytes, 0);
|
|
206
|
+
return {
|
|
207
|
+
data: mergedData,
|
|
208
|
+
metadata: {
|
|
209
|
+
sizeBytes: totalSize,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
108
212
|
}
|
|
109
213
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "irdata_js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "JavaScript library to interact with the iRacing /data API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,10 +20,7 @@
|
|
|
20
20
|
"format": "prettier --write .",
|
|
21
21
|
"prepublishOnly": "npm test && npm run build"
|
|
22
22
|
},
|
|
23
|
-
"repository":
|
|
24
|
-
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/popmonkey/irdata_js.git"
|
|
26
|
-
},
|
|
23
|
+
"repository": "popmonkey/irdata_js",
|
|
27
24
|
"keywords": [
|
|
28
25
|
"iracing",
|
|
29
26
|
"api",
|
|
@@ -31,7 +28,11 @@
|
|
|
31
28
|
"sdk"
|
|
32
29
|
],
|
|
33
30
|
"author": "",
|
|
34
|
-
"license": "
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"registry": "https://registry.npmjs.org/",
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
35
36
|
"bugs": {
|
|
36
37
|
"url": "https://github.com/popmonkey/irdata_js/issues"
|
|
37
38
|
},
|