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 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 and automatically follows S3 links if returned by the API.
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 memberInfo = await client.getData('/member/info');
63
- console.log(memberInfo);
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
- ISC
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
- getData<T>(endpoint: string): Promise<T>;
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
- * Performs a fetch request with authentication headers.
14
- * Does NOT automatically follow "link" responses.
24
+ * Internal method to perform request and return data + size.
15
25
  */
16
- async request(endpoint, options = {}) {
17
- // Remove leading slash from endpoint if present to avoid double slashes if apiUrl has trailing slash
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
- const response = await fetch(url, mergedOptions);
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
- const retryResponse = await fetch(url, retryOptions);
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
- return this.handleErrorResponse(response);
64
+ if (!response.ok) {
65
+ return this.handleErrorResponse(response);
66
+ }
61
67
  }
62
- return response.json();
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.request(endpoint);
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 (data &&
87
- typeof data === 'object' &&
88
- 'link' in data &&
89
- typeof data.link === 'string') {
90
- const s3Link = data.link;
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
- // Fetch the S3 link without original auth headers
93
- let fetchUrl = s3Link;
94
- if (this.fileProxyUrl) {
95
- // If a file proxy is configured, use it
96
- // e.g. http://localhost:80/passthrough?url=...
97
- const separator = this.fileProxyUrl.includes('?') ? '&' : '?';
98
- fetchUrl = `${this.fileProxyUrl}${separator}url=${encodeURIComponent(s3Link)}`;
99
- }
100
- const linkResponse = await fetch(fetchUrl);
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 data;
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.1.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": "ISC",
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
  },