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";
8 isInternetConnected, CURRENT_DATA_VERSION
9 } from "./downloadHelpers.mjs";
10 import { join as joinPosix } from "path/posix";
11 import { readFileSync } from "fs";
14 * Tells if the stored data is a GithubReleaseResponse (data of a specific release)
15 * or a string array (release tags).
17 export enum GithubApiCacheEntryDataType {
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;
30 data: GithubReleaseResponse | string[];
32 /// Use to check if the cached data is still up to date.
37 const CACHE_JSON_URL =
38 "https://raspberrypi.github.io/pico-vscode/" +
39 `${CURRENT_DATA_VERSION}/github-cache.json`;
42 function parseCacheJson(
44 ): { [id: string] : GithubReleaseResponse | string[] } {
49 ) as { [id: string] : GithubReleaseResponse | string[] };
53 Logger.log("Failed to parse github-cache.json");
56 "Error while downloading github cache. " +
62 export async function defaultCacheOfRepository(
63 repository: GithubRepository,
64 dataType: GithubApiCacheEntryDataType,
66 ): Promise<GithubApiCacheEntry | undefined> {
67 const ret: GithubApiCacheEntry = {
68 repository: repository,
74 if (!(await isInternetConnected())) {
76 "Error while downloading github cache. " +
77 "No internet connection"
80 const result = await new Promise<
81 { [id: string] : GithubReleaseResponse | string[] }
83 (resolve, reject) => {
84 // Download the JSON file
85 get(CACHE_JSON_URL, response => {
86 if (response.statusCode !== 200) {
89 "Error while downloading github cache list. " +
90 `Status code: ${response.statusCode}`
96 // Append data as it arrives
97 response.on("data", chunk => {
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) {
110 "Error while downloading github cache list. " +
111 "Parsing data failed"
118 response.on("error", error => {
125 // TODO: Logger.debug
126 Logger.log(`Successfully downloaded github cache from the internet.`);
129 `githubApiCache-${repository}-${dataType}`
130 + `${tag !== undefined ? '-' + tag : ''}`
135 Logger.log(error instanceof Error ? error.message : (error as string));
137 Logger.log("Failed to load github-cache.json");
140 const cacheFile = readFileSync(
141 joinPosix(getDataRoot(), "github-cache.json")
143 const parsed = parseCacheJson(cacheFile.toString("utf-8"));
145 `githubApiCache-${repository}-${dataType}`
146 + `${tag !== undefined ? '-' + tag : ''}`
151 Logger.log("Failed to load local github-cache.json");
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.
162 export default class GithubApiCache {
163 private static instance?: GithubApiCache;
164 private globalState: Memento;
166 private constructor(context: ExtensionContext) {
167 this.globalState = context.globalState;
171 * Creates a new instance of the GithubApiCache as singleton.
173 * @param context The extension context.
174 * @returns A reference to the created singleton instance.
176 public static createInstance(context: ExtensionContext): GithubApiCache {
177 GithubApiCache.instance = new GithubApiCache(context);
179 return GithubApiCache.instance;
183 * Returns the singleton instance of the GithubApiCache if available.
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.
189 public static getInstance(): GithubApiCache {
190 if (GithubApiCache.instance === undefined) {
191 throw new Error("GithubApiCache not initialized.");
194 return GithubApiCache.instance;
198 * Saves the given response in the cache together with its etag.
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.
206 public async saveResponse(
207 repository: GithubRepository,
208 dataType: GithubApiCacheEntryDataType,
209 data: GithubReleaseResponse | string[],
213 if (etag === undefined) {
215 Logger.log("GithubApiCache.saveResponse: response without etag.");
220 await this.globalState.update(
221 `githubApiCache-${repository}-${dataType}`
222 + `${tag !== undefined ? '-' + tag : ''}`, {
227 } as GithubApiCacheEntry);
230 public async getResponse(
231 repository: GithubRepository,
232 dataType: GithubApiCacheEntryDataType,
234 ): Promise<GithubApiCacheEntry | undefined> {
235 return this.globalState.get(
236 `githubApiCache-${repository}-${dataType}`
237 + `${tag !== undefined ? '-' + tag : ''}`
242 * Returns the last etag of the given repository and data type.
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.
249 public async getLastEtag(
250 repository: GithubRepository,
251 dataType: GithubApiCacheEntryDataType,
253 ): Promise<string | undefined> {
254 const response = await this.getResponse(repository, dataType, tag);
256 return response?.etag;
259 public async getDefaultResponse(
260 repository: GithubRepository,
261 dataType: GithubApiCacheEntryDataType,
263 ): Promise<GithubApiCacheEntry | undefined> {
264 const lastEtag = await GithubApiCache.getInstance().getLastEtag(
269 return this.globalState.get(
270 `githubApiCache-${repository}-${dataType}`
271 + `${tag !== undefined ? '-' + tag : ''}`
274 return defaultCacheOfRepository(repository, dataType, tag);
279 * Clears the github api cache.
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);