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