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";
10 } from "./downloadHelpers.mjs";
11 import { join as joinPosix } from "path/posix";
12 import { readFileSync } from "fs";
15 * Tells if the stored data is a GithubReleaseResponse (data of a specific release)
16 * or a string array (release tags).
18 export enum GithubApiCacheEntryDataType {
23 /// The data structure containing the cached data of a Github API request.
24 export interface GithubApiCacheEntry {
25 /// Identity of entry is a combination of repository and dataType.
26 repository: GithubRepository;
27 /// Identity of entry is a combination of repository and dataType.
28 dataType: GithubApiCacheEntryDataType;
31 data: GithubReleaseResponse | string[];
33 /// 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`;
41 function parseCacheJson(data: string): {
42 [id: string]: GithubReleaseResponse | string[];
45 const cache = JSON.parse(data.toString()) as {
46 [id: string]: GithubReleaseResponse | string[];
51 Logger.log("Failed to parse github-cache.json");
54 "Error while downloading github cache. " + "Parsing Failed"
59 export async function defaultCacheOfRepository(
60 repository: GithubRepository,
61 dataType: GithubApiCacheEntryDataType,
63 ): Promise<GithubApiCacheEntry | undefined> {
64 const ret: GithubApiCacheEntry = {
65 repository: repository,
71 if (!(await isInternetConnected())) {
73 "Error while downloading github cache. " + "No internet connection"
76 const result = await new Promise<{
77 [id: string]: GithubReleaseResponse | string[];
78 }>((resolve, reject) => {
79 // Download the JSON file
80 get(CACHE_JSON_URL, response => {
81 if (response.statusCode !== 200) {
84 "Error while downloading github cache list. " +
85 `Status code: ${response.statusCode}`
91 // Append data as it arrives
92 response.on("data", chunk => {
96 // Parse the JSON data when the download is complete
97 response.on("end", () => {
98 // Resolve with the array of SupportedToolchainVersion
99 const ret = parseCacheJson(data);
100 if (ret !== undefined) {
105 "Error while downloading github cache list. " +
106 "Parsing data failed"
113 response.on("error", error => {
119 // TODO: Logger.debug
120 Logger.log(`Successfully downloaded github cache from the internet.`);
124 `githubApiCache-${repository}-${dataType}` +
125 `${tag !== undefined ? "-" + tag : ""}`
130 Logger.log(error instanceof Error ? error.message : (error as string));
132 Logger.log("Failed to load github-cache.json");
135 const cacheFile = readFileSync(
136 joinPosix(getDataRoot(), "github-cache.json")
138 const parsed = parseCacheJson(cacheFile.toString("utf-8"));
141 `githubApiCache-${repository}-${dataType}` +
142 `${tag !== undefined ? "-" + tag : ""}`
147 Logger.log("Failed to load local github-cache.json");
155 * A simple cache for Github API responses to avoid hittign the rate limit,
156 * and to avoid putting unnecessary load on the Github servers.
158 export default class GithubApiCache {
159 private static instance?: GithubApiCache;
160 private globalState: Memento;
162 private constructor(context: ExtensionContext) {
163 this.globalState = context.globalState;
167 * Creates a new instance of the GithubApiCache as singleton.
169 * @param context The extension context.
170 * @returns A reference to the created singleton instance.
172 public static createInstance(context: ExtensionContext): GithubApiCache {
173 GithubApiCache.instance = new GithubApiCache(context);
175 return GithubApiCache.instance;
179 * Returns the singleton instance of the GithubApiCache if available.
181 * @throws An error if the singleton instance is not available. It can be created
182 * by calling createInstance() with the extension context.
183 * @returns The requested singleton instance or throws an error if the instance is not available.
185 public static getInstance(): GithubApiCache {
186 if (GithubApiCache.instance === undefined) {
187 throw new Error("GithubApiCache not initialized.");
190 return GithubApiCache.instance;
194 * Saves the given response in the cache together with its etag.
196 * @param repository The repository the response is for.
197 * @param dataType The type of the request (for this repository) the response is from.
198 * @param data The response data to save.
199 * @param etag The etag of the response.
200 * @returns A promise that resolves when the response is saved.
202 public async saveResponse(
203 repository: GithubRepository,
204 dataType: GithubApiCacheEntryDataType,
205 data: GithubReleaseResponse | string[],
209 if (etag === undefined) {
211 Logger.log("GithubApiCache.saveResponse: response without etag.");
216 await this.globalState.update(
217 `githubApiCache-${repository}-${dataType}` +
218 `${tag !== undefined ? "-" + tag : ""}`,
224 } as GithubApiCacheEntry
228 public async getResponse(
229 repository: GithubRepository,
230 dataType: GithubApiCacheEntryDataType,
232 ): Promise<GithubApiCacheEntry | undefined> {
233 return this.globalState.get(
234 `githubApiCache-${repository}-${dataType}` +
235 `${tag !== undefined ? "-" + tag : ""}`
240 * Returns the last etag of the given repository and data type.
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.
247 public async getLastEtag(
248 repository: GithubRepository,
249 dataType: GithubApiCacheEntryDataType,
251 ): Promise<string | undefined> {
252 const response = await this.getResponse(repository, dataType, tag);
254 return response?.etag;
257 public async getDefaultResponse(
258 repository: GithubRepository,
259 dataType: GithubApiCacheEntryDataType,
261 ): Promise<GithubApiCacheEntry | undefined> {
262 const lastEtag = await GithubApiCache.getInstance().getLastEtag(
267 return this.globalState.get(
268 `githubApiCache-${repository}-${dataType}` +
269 `${tag !== undefined ? "-" + tag : ""}`
272 return defaultCacheOfRepository(repository, dataType, tag);
277 * Clears the github api cache.
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);