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";
12 * Tells if the stored data is a GithubReleaseResponse (data of a specific release)
13 * or a string array (release tags).
15 export enum GithubApiCacheEntryDataType {
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;
28 data: GithubReleaseResponse | string[];
30 /// Use to check if the cached data is still up to date.
35 const CACHE_JSON_URL =
36 "https://raspberrypi.github.io/pico-vscode/" +
37 `${CURRENT_DATA_VERSION}/github-cache.json`;
40 function parseCacheJson(
42 ): { [id: string] : GithubReleaseResponse | string[] } {
47 ) as { [id: string] : GithubReleaseResponse | string[] };
51 Logger.log("Failed to parse github-cache.json");
54 "Error while downloading github cache. " +
60 export async function defaultCacheOfRepository(
61 repository: GithubRepository,
62 dataType: GithubApiCacheEntryDataType,
64 ): Promise<GithubApiCacheEntry | undefined> {
65 const ret: GithubApiCacheEntry = {
66 repository: repository,
72 if (!(await isInternetConnected())) {
74 "Error while downloading github cache. " +
75 "No internet connection"
78 const result = await new Promise<
79 { [id: string] : GithubReleaseResponse | string[] }
81 (resolve, reject) => {
82 // Download the JSON file
83 get(CACHE_JSON_URL, response => {
84 if (response.statusCode !== 200) {
87 "Error while downloading github cache list. " +
88 `Status code: ${response.statusCode}`
94 // Append data as it arrives
95 response.on("data", chunk => {
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) {
108 "Error while downloading github cache list. " +
109 "Parsing data failed"
116 response.on("error", error => {
123 // TODO: Logger.debug
124 Logger.log(`Successfully downloaded github cache from the internet.`);
127 `githubApiCache-${repository}-${dataType}`
128 + `${tag !== undefined ? '-' + tag : ''}`
133 Logger.log(error instanceof Error ? error.message : (error as string));
135 Logger.log("Failed to load github-cache.json");
138 const cacheFile = readFileSync(
139 joinPosix(getDataRoot(), "github-cache.json")
141 const parsed = parseCacheJson(cacheFile.toString("utf-8"));
143 `githubApiCache-${repository}-${dataType}`
144 + `${tag !== undefined ? '-' + tag : ''}`
149 Logger.log("Failed to load github-cache.json");
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.
160 export default class GithubApiCache {
161 private static instance?: GithubApiCache;
162 private globalState: Memento;
164 private constructor(context: ExtensionContext) {
165 this.globalState = context.globalState;
169 * Creates a new instance of the GithubApiCache as singleton.
171 * @param context The extension context.
172 * @returns A reference to the created singleton instance.
174 public static createInstance(context: ExtensionContext): GithubApiCache {
175 GithubApiCache.instance = new GithubApiCache(context);
177 return GithubApiCache.instance;
181 * Returns the singleton instance of the GithubApiCache if available.
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.
187 public static getInstance(): GithubApiCache {
188 if (GithubApiCache.instance === undefined) {
189 throw new Error("GithubApiCache not initialized.");
192 return GithubApiCache.instance;
196 * Saves the given response in the cache together with its etag.
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.
204 public async saveResponse(
205 repository: GithubRepository,
206 dataType: GithubApiCacheEntryDataType,
207 data: GithubReleaseResponse | string[],
211 if (etag === undefined) {
213 Logger.log("GithubApiCache.saveResponse: response without etag.");
218 await this.globalState.update(
219 `githubApiCache-${repository}-${dataType}`
220 + `${tag !== undefined ? '-' + tag : ''}`, {
225 } 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);