import type { Readable, Subscriber, Unsubscriber, Writable } from "svelte/store"; import { writable } from "svelte/store"; import { error } from "@sveltejs/kit"; import { prerendering, browser, dev } from "$app/environment"; import * as settings from "./settings"; import * as cache from "./cache"; export class API implements Readable { private store: Writable; // True if we have or are about to request data from the API. has_requested: boolean; // `transform` will transform the data received from the API. constructor(public readonly endpoint: string, private readonly default_value: T, private readonly transform: ((v: any) => T) = (v) => v as T) { // Initialize with cached data if possible. const cached_data = cache.get(this.endpoint); this.has_requested = cached_data !== null; this.store = writable(cached_data || this.default_value); } private url() { return `${settings.api_base_url()}/${this.endpoint}`; } // Please don't call this directly private async _update(fetch_fn: typeof fetch) { // Try to get data from the cache. let data = cache.get(this.endpoint); if (data === null) { // Fetch and transform data const response = await fetch_fn(this.url()); data = this.transform(await response.json()); // Update the cache. cache.update(this.endpoint, data); } this.store.set(data); } // Retrieve data and update. private update(fetch_fn = fetch) { // Make sure we set this immediately outside of the async function to avoid JS event loop weirdness. this.has_requested = true; return this._update(fetch_fn); } // Start retrieving data if needed. retrieve_if_needed() { if (!this.has_requested) { return this.update(); } return Promise.resolve() } // Implements the load function found in `+page/layout.ts` files. page_load_impl() { return async ({ fetch }) => { if (prerendering) { return {}; } // Might be better to actually return some data from the load function and use that on the client. if (!(dev || browser || prerendering)) { throw new Error("The API client is not optimized for production server-side rendering. Please change that :)"); } try { await this.update(fetch); return {}; } catch(e) { console.error(e); throw error(504, "API Request Error"); } }; } // Implement Svelte store. subscribe(run: Subscriber, invalidate?: any): Unsubscriber { // Make sure we have up-to-date data from the API. if (browser) { this.retrieve_if_needed(); } return this.store.subscribe(run, invalidate); } } // API Endpoints import type { Patch, Repository, Tool } from '../types'; import { dev_log } from "$lib/utils"; export type ReposData = Repository[]; export type PatchesData = { patches: Patch[]; packages: string[] }; export type ToolsData = { [repo: string]: Tool }; export const repositories = new API("contributors", [], json => json.repositories); // It needs to look this way to not break everything. const tools_placeholder: ToolsData = { "revanced/revanced-manager": { version: "v0.0.0", timestamp: "", repository: "", assets: [{ url: "", name: "", content_type: "", size: null, }] } } export const tools = new API("tools", tools_placeholder, json => { // The API returns data in a weird shape. Make it easier to work with. let map: Map = new Map(); for (const tool of json["tools"]) { const repo: string = tool.repository; if (!map.has(repo)) { map.set(repo, { version: tool.version, repository: repo, // Just use the timestamp of the first one we find. timestamp: tool.timestamp, assets: [] }); } let value = map.get(repo); value.assets.push({ name: tool.name, size: tool.size, url: tool.browser_download_url, content_type: tool.content_type }); map.set(repo, value); } return Object.fromEntries(map); }); export const patches = new API("patches", { patches: [], packages: [] }, patches => { let packages: string[] = []; // gets packages for (let i = 0; i < patches.length; i++) { patches[i].compatiblePackages.forEach((pkg: Patch) => { let index = packages.findIndex((x) => x == pkg.name); if (index === -1) { packages.push(pkg.name); } }); } return { patches, packages }; });