]> Git Repo - pico-vscode.git/blob - src/utils/githubApiCache.mts
Add rp2350 support
[pico-vscode.git] / src / utils / githubApiCache.mts
1 // TODO: put defaults into json file, to prevent need for these disables
2 import type { ExtensionContext, Memento } from "vscode";
3 import type { GithubReleaseResponse, GithubRepository } from "./githubREST.mjs";
4 import Logger from "../logger.mjs";
5 import { getDataRoot } from "./downloadHelpers.mjs";
6 import { get } from "https";
7 import {
8   isInternetConnected, CURRENT_DATA_VERSION
9 } from "./downloadHelpers.mjs";
10 import { join as joinPosix } from "path/posix";
11 import { readFileSync } from "fs";
12
13 /**
14  * Tells if the stored data is a GithubReleaseResponse (data of a specific release)
15  * or a string array (release tags).
16  */
17 export enum GithubApiCacheEntryDataType {
18   releases = 0,
19   tag = 1,
20 }
21
22 /// The data structure containing the cached data of a Github API request.
23 export interface GithubApiCacheEntry {
24   /// Identity of entry is a combination of repository and dataType.
25   repository: GithubRepository;
26   /// Identity of entry is a combination of repository and dataType.
27   dataType: GithubApiCacheEntryDataType;
28
29   /// The cached data.
30   data: GithubReleaseResponse | string[];
31
32   /// Use to check if the cached data is still up to date.
33   etag: string;
34 }
35
36
37 const CACHE_JSON_URL =
38   "https://raspberrypi.github.io/pico-vscode/" +
39   `${CURRENT_DATA_VERSION}/github-cache.json`;
40
41
42 function parseCacheJson(
43   data: string
44 ): { [id: string] : GithubReleaseResponse | string[] } {
45   try {
46     const cache =
47       JSON.parse(
48         data.toString()
49       ) as { [id: string] : GithubReleaseResponse | string[] };
50
51     return cache;
52   } catch {
53     Logger.log("Failed to parse github-cache.json");
54
55     throw new Error(
56       "Error while downloading github cache. " +
57         "Parsing Failed"
58     );
59   }
60 }
61
62 export async function defaultCacheOfRepository(
63   repository: GithubRepository,
64   dataType: GithubApiCacheEntryDataType,
65   tag?: string
66 ): Promise<GithubApiCacheEntry | undefined> {
67   const ret: GithubApiCacheEntry = {
68     repository: repository,
69     dataType: dataType,
70     data: [],
71     etag: "",
72   };
73   try {
74     if (!(await isInternetConnected())) {
75       throw new Error(
76         "Error while downloading github cache. " +
77           "No internet connection"
78       );
79     }
80     const result = await new Promise<
81       { [id: string] : GithubReleaseResponse | string[] }
82     >(
83       (resolve, reject) => {
84         // Download the JSON file
85         get(CACHE_JSON_URL, response => {
86           if (response.statusCode !== 200) {
87             reject(
88               new Error(
89                 "Error while downloading github cache list. " +
90                   `Status code: ${response.statusCode}`
91               )
92             );
93           }
94           let data = "";
95
96           // Append data as it arrives
97           response.on("data", chunk => {
98             data += chunk;
99           });
100
101           // Parse the JSON data when the download is complete
102           response.on("end", () => {
103             // Resolve with the array of SupportedToolchainVersion
104             const ret = parseCacheJson(data);
105             if (ret !== undefined) {
106               resolve(ret);
107             } else {
108               reject(
109                 new Error(
110                   "Error while downloading github cache list. " +
111                     "Parsing data failed"
112                 )
113               );
114             }
115           });
116
117           // Handle errors
118           response.on("error", error => {
119             reject(error);
120           });
121         });
122       }
123     );
124
125     // TODO: Logger.debug
126     Logger.log(`Successfully downloaded github cache from the internet.`);
127
128     ret.data = result[
129       `githubApiCache-${repository}-${dataType}`
130       + `${tag !== undefined ? '-' + tag : ''}`
131     ];
132
133     return ret;
134   } catch (error) {
135     Logger.log(error instanceof Error ? error.message : (error as string));
136
137     Logger.log("Failed to load github-cache.json");
138
139     try {
140       const cacheFile = readFileSync(
141         joinPosix(getDataRoot(), "github-cache.json")
142       );
143       const parsed = parseCacheJson(cacheFile.toString("utf-8"));
144       ret.data = parsed[
145         `githubApiCache-${repository}-${dataType}`
146         + `${tag !== undefined ? '-' + tag : ''}`
147       ];
148
149       return ret;
150     } catch (e) {
151       Logger.log("Failed to load local github-cache.json");
152
153       return undefined;
154     }
155   }
156 }
157
158 /**
159  * A simple cache for Github API responses to avoid hittign the rate limit,
160  * and to avoid putting unnecessary load on the Github servers.
161  */
162 export default class GithubApiCache {
163   private static instance?: GithubApiCache;
164   private globalState: Memento;
165
166   private constructor(context: ExtensionContext) {
167     this.globalState = context.globalState;
168   }
169
170   /**
171    * Creates a new instance of the GithubApiCache as singleton.
172    *
173    * @param context The extension context.
174    * @returns A reference to the created singleton instance.
175    */
176   public static createInstance(context: ExtensionContext): GithubApiCache {
177     GithubApiCache.instance = new GithubApiCache(context);
178
179     return GithubApiCache.instance;
180   }
181
182   /**
183    * Returns the singleton instance of the GithubApiCache if available.
184    *
185    * @throws An error if the singleton instance is not available. It can be created
186    * by calling createInstance() with the extension context.
187    * @returns The requested singleton instance or throws an error if the instance is not available.
188    */
189   public static getInstance(): GithubApiCache {
190     if (GithubApiCache.instance === undefined) {
191       throw new Error("GithubApiCache not initialized.");
192     }
193
194     return GithubApiCache.instance;
195   }
196
197   /**
198    * Saves the given response in the cache together with its etag.
199    *
200    * @param repository The repository the response is for.
201    * @param dataType The type of the request (for this repository) the response is from.
202    * @param data The response data to save.
203    * @param etag The etag of the response.
204    * @returns A promise that resolves when the response is saved.
205    */
206   public async saveResponse(
207     repository: GithubRepository,
208     dataType: GithubApiCacheEntryDataType,
209     data: GithubReleaseResponse | string[],
210     tag?: string,
211     etag?: string
212   ): Promise<void> {
213     if (etag === undefined) {
214       // TODO: Logger.warn
215       Logger.log("GithubApiCache.saveResponse: response without etag.");
216
217       return;
218     }
219
220     await this.globalState.update(
221       `githubApiCache-${repository}-${dataType}`
222       + `${tag !== undefined ? '-' + tag : ''}`, {
223         repository,
224         dataType,
225         data,
226         etag,
227     } as GithubApiCacheEntry);
228   }
229
230   public async getResponse(
231     repository: GithubRepository,
232     dataType: GithubApiCacheEntryDataType,
233     tag?: string
234   ): Promise<GithubApiCacheEntry | undefined> {
235     return this.globalState.get(
236       `githubApiCache-${repository}-${dataType}`
237       + `${tag !== undefined ? '-' + tag : ''}`
238     );
239   }
240
241   /**
242    * Returns the last etag of the given repository and data type.
243    *
244    * @param repository The repository to get the last etag for.
245    * @param dataType The type of request (for this repository) to get the last etag for.
246    * @returns The last etag of the given repository and data type combination or
247    * undefined if no etag is stored.
248    */
249   public async getLastEtag(
250     repository: GithubRepository,
251     dataType: GithubApiCacheEntryDataType,
252     tag?: string
253   ): Promise<string | undefined> {
254     const response = await this.getResponse(repository, dataType, tag);
255
256     return response?.etag;
257   }
258
259   public async getDefaultResponse(
260     repository: GithubRepository,
261     dataType: GithubApiCacheEntryDataType,
262     tag?: string
263   ): Promise<GithubApiCacheEntry | undefined> {
264     const lastEtag = await GithubApiCache.getInstance().getLastEtag(
265       repository,
266       dataType
267     );
268     if (lastEtag) {
269       return this.globalState.get(
270         `githubApiCache-${repository}-${dataType}`
271         + `${tag !== undefined ? '-' + tag : ''}`
272       );
273     } else {
274       return defaultCacheOfRepository(repository, dataType, tag);
275     }
276   }
277
278   /**
279    * Clears the github api cache.
280    */
281   public async clear(): Promise<void> {
282     // for all keys in global state starting with githubApiCache- delete
283     for (const key of this.globalState.keys()) {
284       if (key.startsWith("githubApiCache-")) {
285         await this.globalState.update(key, undefined);
286       }
287     }
288   }
289 }
This page took 0.040501 seconds and 4 git commands to generate.