From b23e84043d27a9683a27678bdc396a54ff612fc6 Mon Sep 17 00:00:00 2001 From: Ax333l Date: Sat, 29 Oct 2022 19:32:04 +0200 Subject: [PATCH] feat: api data layer --- src/data/api/cache.ts | 51 +++++++ src/data/api/index.ts | 144 ++++++++++++++++++++ src/data/api/settings.ts | 22 +++ src/lib/components/atoms/NavButton.svelte | 3 +- src/lib/components/molecules/NavHost.svelte | 1 + src/lib/utils.ts | 18 +-- src/routes/+layout.server.ts | 15 -- src/routes/+layout.svelte | 7 + src/routes/+layout.ts | 7 + src/routes/api-settings/+page.svelte | 26 ++++ src/routes/credits/+page.svelte | 13 +- src/routes/patches/+page.server.ts | 26 ---- src/routes/patches/+page.svelte | 10 +- src/routes/patches/+page.ts | 5 + 14 files changed, 283 insertions(+), 65 deletions(-) create mode 100644 src/data/api/cache.ts create mode 100644 src/data/api/index.ts create mode 100644 src/data/api/settings.ts delete mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/+layout.ts create mode 100644 src/routes/api-settings/+page.svelte delete mode 100644 src/routes/patches/+page.server.ts create mode 100644 src/routes/patches/+page.ts diff --git a/src/data/api/cache.ts b/src/data/api/cache.ts new file mode 100644 index 0000000..edc61fa --- /dev/null +++ b/src/data/api/cache.ts @@ -0,0 +1,51 @@ +import { browser } from "$app/environment"; + +import { dev_log } from "$lib/utils"; + +const CACHE_KEY_PREFIX = "revanced_api_cache_l1"; +const L1_CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes + +function l1_key_name(endpoint: string) { + return `${CACHE_KEY_PREFIX}:${endpoint}`; +} + +// Get item from the cache +export function get(endpoint: string) { + if (!browser) { + return null; + } + + const key_name = l1_key_name(endpoint); + const ls_data: { valid_until: number; data: any } | null = JSON.parse(localStorage.getItem(key_name)); + + if (ls_data === null || ls_data.valid_until <= Date.now()) { + dev_log("Cache", `missed "${endpoint}"`); + localStorage.removeItem(key_name); + return null; + } + + + dev_log("Cache", `hit "${endpoint}"`); + return ls_data.data; +} + +// Update the cache +export function update(endpoint: string, data: any) { + if (!browser) { + return; + } + + localStorage.setItem(l1_key_name(endpoint), JSON.stringify({ + data, + valid_until: Date.now() + L1_CACHE_VALIDITY + })); +} + +// Clear the cache +export function clear() { + for (const key of Object.keys(localStorage)) { + if (key.startsWith(CACHE_KEY_PREFIX)) { + localStorage.removeItem(key); + } + } +} diff --git a/src/data/api/index.ts b/src/data/api/index.ts new file mode 100644 index 0000000..9da5310 --- /dev/null +++ b/src/data/api/index.ts @@ -0,0 +1,144 @@ +import type { Readable, Subscriber, Unsubscriber, Writable } from "svelte/store"; +import { writable } from "svelte/store"; +import { error } from "@sveltejs/kit"; + +import { prerendering, browser } from "$app/environment"; + +import * as settings from "./settings"; +import * as cache from "./cache"; + + +export class API implements Readable { + private store: Writable; + // Note: transform function will not be called on cache hit. + private transform: (v: any) => T; + // True if we have or are about to request data from the possibly user-specified API. + private requested_from_api = false; + + // If `transform_fn_or_key` is unspecified, the data will be returned from the API as is. + // If `transform_fn_or_key` is a function, the JSON data will pass through it. + // If `transform_fn_or_key` is a string, the JSON data will be assigned to a prop on an object. + // If `load_fn_fallback` is not specified, the load function will instead cause HTTP error 500 if the API request fails (it will always throw if prerendering). + constructor(public readonly endpoint: string, transform_fn_or_key?: ((v: any) => T) | string, private load_fn_fallback?: T) { + if (transform_fn_or_key === undefined) { + this.transform = (v) => v as T; + } else if (typeof transform_fn_or_key != "string") { + // `transform_fn_or_key` is function. + this.transform = transform_fn_or_key; + } else { + // `transform_fn_or_key` is string. + this.transform = (v) => { + let data = {}; + data[transform_fn_or_key] = v; + return data as T; + }; + } + } + + private url(): string { + let url = `${settings.api_base_url()}/${this.endpoint}`; + + if (prerendering) { + url += '?cacheBypass='; + // Just add some random stuff to the string. Doesn't really matter what we add. + // This is here to make sure we bypass the cache while prerendering. + for (let i = 0; i < 6; i++) { + url += Math.floor(Math.random() * 10).toString(); + } + } + + return url; + } + + initialized() { + return this.store !== undefined; + } + + // Initialize if needed + init(data: T) { + if (this.initialized()) { + return; + } + + this.store = writable(data); + } + + // Request data, transform, cache and initialize if necessary. + async request(fetch_fn = fetch): Promise { + if (browser) { + this.requested_from_api = true; + } + + // 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); + } + + // Initialize with the data. Applicable when page load function runs on client. + this.init(data); + + // store_in_cache(data)... + + return data; + } + + // Implements the load function found in `+page/layout.ts` files. + page_load_impl() { + return async ({ fetch }) => { + try { + return await this.request(fetch); + } catch(e) { + console.error(e); + if (this.load_fn_fallback !== undefined && !prerendering) { + return this.load_fn_fallback; + } + throw error(500, "API Request Error"); + } + }; + } + + // Implement Svelte store. + subscribe(run: Subscriber, invalidate?: any): Unsubscriber { + if (!this.initialized()) { + // Make sure you call .init() with data from the load() function of the page you are working on or a layout above it. + throw Error(`API "${this.endpoint}" has not been initialized yet.`); + } + // Make sure we have up-to-date data from the API. + if (!this.requested_from_api && browser) { + this.request().then(this.store.set); + } + + return this.store.subscribe(run, invalidate); + } +} + +// API Endpoints +import type { Patch, Repository } from '../types'; +import { dev_log } from "$lib/utils"; + +export type ContribData = { repositories: Repository[] }; +export type PatchesData = { patches: Patch[]; packages: string[] }; + +export const contributors = new API("contributors", undefined, { repositories: [] }); + +export const patches = new API("patches", 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 }; +}); diff --git a/src/data/api/settings.ts b/src/data/api/settings.ts new file mode 100644 index 0000000..6760c20 --- /dev/null +++ b/src/data/api/settings.ts @@ -0,0 +1,22 @@ +import { browser } from "$app/environment"; + +const URL_KEY = "revanced_api_url"; + +// Get base URL +export function api_base_url(): string { + const default_base_url = "https://releases.revanced.app"; + if (browser) { + return localStorage.getItem(URL_KEY) || default_base_url; + } + + return default_base_url; +} + +// (re)set base URL. +export function set_api_base_url(url?: string) { + if (!url) { + localStorage.removeItem(URL_KEY); + } else { + localStorage.setItem(URL_KEY, url); + } +} diff --git a/src/lib/components/atoms/NavButton.svelte b/src/lib/components/atoms/NavButton.svelte index 9c2ea36..3530d70 100644 --- a/src/lib/components/atoms/NavButton.svelte +++ b/src/lib/components/atoms/NavButton.svelte @@ -1,10 +1,11 @@ -
  • +
  • diff --git a/src/lib/components/molecules/NavHost.svelte b/src/lib/components/molecules/NavHost.svelte index afc76bb..bddf13d 100644 --- a/src/lib/components/molecules/NavHost.svelte +++ b/src/lib/components/molecules/NavHost.svelte @@ -33,6 +33,7 @@ Docs Patches Credits + API Settings