Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use Workers for manifest fetching & optimizations #572

Merged
merged 28 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
215f882
feat(fetch-remotes): use Github Contents API
kyrie25 Aug 5, 2023
d18cea7
style: lint
kyrie25 Aug 5, 2023
c606a89
Update FetchRemotes.ts
kyrie25 Aug 5, 2023
a810750
fix: display error handling
kyrie25 Aug 5, 2023
9f76571
Update marketplace-types.d.ts
kyrie25 Aug 5, 2023
ca6779a
chore: types
kyrie25 Aug 5, 2023
fc96bd6
Merge branch 'feat/fetch-gh-contents' of https://github.com/spicetify…
kyrie25 Aug 5, 2023
2d2249e
fix: workaround rate limit when switching tabs
kyrie25 Aug 5, 2023
8e1809b
perf: use cached repo results
kyrie25 Aug 5, 2023
aa35cdc
style: header alignment
kyrie25 Aug 5, 2023
fbeb29c
Merge branch 'main' into feat/fetch-gh-contents
theRealPadster Aug 5, 2023
4964bfd
perf: cache tld & display warning
kyrie25 Aug 6, 2023
578d4fb
Merge branch 'feat/fetch-gh-contents' of https://github.com/spicetify…
kyrie25 Aug 6, 2023
a39274e
feat: display notification if fail to connect to CDN
kyrie25 Aug 6, 2023
c721038
chore: move comment
kyrie25 Aug 6, 2023
33e5aa0
fix: working cache from preload
kyrie25 Aug 6, 2023
760a124
refactor: simplify
kyrie25 Aug 6, 2023
2aab6e6
perf: fix/optimize cache & remove duplicate requests
kyrie25 Aug 6, 2023
b68f212
chore: remove log
kyrie25 Aug 6, 2023
8b862be
refactor: simplify
kyrie25 Aug 6, 2023
96da572
revert: use raw file URL
kyrie25 Aug 6, 2023
f9d90f4
chore: cleanup
kyrie25 Aug 6, 2023
37111f2
chore: cleanup
kyrie25 Aug 6, 2023
f7a7fe0
feat: use Workers for manifest fetch
kyrie25 Aug 6, 2023
36248ce
fix: value mixing
kyrie25 Aug 6, 2023
cbd137f
Merge branch 'main' into feat/fetch-gh-contents
kyrie25 Aug 7, 2023
0ad0a44
Update src/extensions/extension.tsx
kyrie25 Aug 7, 2023
08787a3
Update src/components/Card/Card.tsx
kyrie25 Aug 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
parseCSS,
injectUserCSS,
generateKey,
getAvailableTLD,
} from "../../logic/Utils";
import TrashIcon from "../Icons/TrashIcon";
import DownloadIcon from "../Icons/DownloadIcon";
Expand Down Expand Up @@ -379,7 +378,7 @@ class Card extends React.Component<CardProps, {
*/
async fetchAndInjectUserCSS(theme) {
try {
const tld = await getAvailableTLD();
const tld = window.sessionStorage.getItem("marketplace-request-tld") || undefined;
const userCSS = theme
? await parseCSS(this.props.item as CardItem, tld)
: undefined;
Expand Down
1 change: 1 addition & 0 deletions src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ class Grid extends React.Component<
// Checks for new Marketplace updates
fetch(LATEST_RELEASE).then(res => res.json()).then(
result => {
if (result.message) throw result;
this.setState({
version: result[0].name,
});
Expand Down
67 changes: 40 additions & 27 deletions src/extensions/extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ import {
parseCSS,
// TODO: there's a slightly different copy of this function in Card.ts?
injectUserCSS,
addToSessionStorage,
sleep,
addExtensionToSpicetifyConfig,
initAlbumArtBasedColor,
getAvailableTLD,
Expand All @@ -26,15 +24,14 @@ import {
getBlacklist,
fetchThemeManifest,
fetchExtensionManifest,
fetchAppManifest,
} from "../logic/FetchRemotes";

(async () => {
(async function init() {
while (!(Spicetify?.LocalStorage && Spicetify?.showNotification)) {
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => setTimeout(resolve, 10));
}

const tld = await getAvailableTLD();

// https://github.com/satya164/react-simple-code-editor/issues/86
const reactSimpleCodeEditorFix = document.createElement("script");
reactSimpleCodeEditorFix.innerHTML = "const global = globalThis;";
Expand All @@ -52,6 +49,8 @@ import {
version: MARKETPLACE_VERSION,
};

const tld = await getAvailableTLD();

const initializeExtension = (extensionKey: string) => {
const extensionManifest = getLocalStorageDataFromKey(extensionKey);
// Abort if no manifest found or no extension URL (i.e. a theme)
Expand Down Expand Up @@ -146,16 +145,30 @@ import {

console.log("Loaded Marketplace extension");

const installedSnippetKeys = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []);
const installedSnippets = installedSnippetKeys.map((key) => getLocalStorageDataFromKey(key));
initializeSnippets(installedSnippets);

if (!tld) {
if (window.navigator.onLine) {
console.error(new Error("Unable to connect to the CDN, please check your Internet configuration."));
Spicetify.showNotification("Marketplace is unable to connect to the CDN. Please check your Internet configuration.", true, 5000);
} else {
// Reload Marketplace extension in case the user couldn't connect to the CDN because they were offline
window.addEventListener("online", init, { once: true });
}

return;
}

window.sessionStorage.setItem("marketplace-request-tld", tld);

// Save to Spicetify.Config for use when removing a theme
Spicetify.Config.local_theme = Spicetify.Config.current_theme;
Spicetify.Config.local_color_scheme = Spicetify.Config.color_scheme;
const installedThemeKey = localStorage.getItem(LOCALSTORAGE_KEYS.themeInstalled);
if (installedThemeKey) initializeTheme(installedThemeKey);

const installedSnippetKeys = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedSnippets, []);
const installedSnippets = installedSnippetKeys.map((key) => getLocalStorageDataFromKey(key));
initializeSnippets(installedSnippets);

const installedExtensions = getLocalStorageDataFromKey(LOCALSTORAGE_KEYS.installedExtensions, []);
installedExtensions.forEach((extensionKey) => initializeExtension(extensionKey));
})();
Expand All @@ -169,16 +182,20 @@ import {
async function queryRepos(type: RepoType, pageNum = 1) {
const BLACKLIST = window.sessionStorage.getItem("marketplace:blacklist");

let url = `https://api.github.com/search/repositories?per_page=${ITEMS_PER_REQUEST}`;
if (type === "extension") url += `&q=${encodeURIComponent("topic:spicetify-extensions")}`;
else if (type === "theme") url += `&q=${encodeURIComponent("topic:spicetify-themes")}`;
let url = `https://api.github.com/search/repositories?per_page=${ITEMS_PER_REQUEST}&q=${encodeURIComponent(`topic:spicetify-${type}s`)}`;
if (pageNum) url += `&page=${pageNum}`;

const allRepos = await fetch(url).then(res => res.json()).catch(() => []);
if (!allRepos.items) {
Spicetify.showNotification("Too Many Requests, Cool Down.", true);
const allRepos = JSON.parse(window.sessionStorage.getItem(`spicetify-${type}s-page-${pageNum}`) || "null") || await fetch(url)
.then(res => res.json())
.catch(() => null);

if (!allRepos?.items) {
Spicetify.showNotification?.("Too Many Requests, Cool Down.", true);
return { items: [] };
}

window.sessionStorage.setItem(`spicetify-${type}s-page-${pageNum}`, JSON.stringify(allRepos));

const filteredResults = {
...allRepos,
page_count: allRepos.items.length,
Expand All @@ -199,7 +216,7 @@ async function loadPageRecursive(type: RepoType, pageNum: number) {
appendInformationToLocalStorage(pageOfRepos, type);

// Sets the amount of items that have thus been fetched
const soFarResults = ITEMS_PER_REQUEST * (pageNum - 1) + pageOfRepos.page_count;
const soFarResults = ITEMS_PER_REQUEST * pageNum + pageOfRepos.page_count;
console.debug({ pageOfRepos });
const remainingResults = pageOfRepos.total_count - soFarResults;

Expand All @@ -221,8 +238,9 @@ async function loadPageRecursive(type: RepoType, pageNum: number) {
// Begin by getting the themes and extensions from github
// const [extensionReposArray, themeReposArray] = await Promise.all([
await Promise.all([
loadPageRecursive("extension", 1),
loadPageRecursive("theme", 1),
loadPageRecursive("extension", 0),
loadPageRecursive("theme", 0),
loadPageRecursive("app", 0),
]);

// let extensionsNextPage = 1;
Expand All @@ -241,13 +259,8 @@ async function loadPageRecursive(type: RepoType, pageNum: number) {
async function appendInformationToLocalStorage(array, type: RepoType) {
// This system should make it so themes and extensions are stored concurrently
for (const repo of array.items) {
// console.log(repo);
const data = (type === "theme")
? await fetchThemeManifest(repo.contents_url, repo.default_branch, repo.stargazers_count)
: await fetchExtensionManifest(repo.contents_url, repo.default_branch, repo.stargazers_count);
if (data) {
addToSessionStorage(data);
await sleep(5000);
}
if (type === "theme") await fetchThemeManifest(repo.contents_url, repo.default_branch, repo.stargazers_count);
else if (type === "extension") await fetchExtensionManifest(repo.contents_url, repo.default_branch, repo.stargazers_count);
else if (type === "app") await fetchAppManifest(repo.contents_url, repo.default_branch, repo.stargazers_count);
}
}
85 changes: 51 additions & 34 deletions src/logic/FetchRemotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ export async function getTaggedRepos(tag: RepoTopic, page = 1, BLACKLIST:string[
// Sorting params (not implemented for Marketplace yet)
// if (sortConfig.by.match(/top|controversial/) && sortConfig.time) {
// url += `&t=${sortConfig.time}`
const allRepos = await fetch(url).then(res => res.json()).catch(() => []);
if (!allRepos.items) {
const allRepos = JSON.parse(window.sessionStorage.getItem(`${tag}-page-${page}`) || "null") || await fetch(url)
.then(res => res.json())
.catch(() => null);

if (!allRepos?.items) {
Spicetify.showNotification("Too Many Requests, Cool Down.", true);
return;
return { items: [] };
}

window.sessionStorage.setItem(`${tag}-page-${page}`, JSON.stringify(allRepos));

const filteredResults = {
...allRepos,
// Include count of all items on the page, since we're filtering the blacklist below,
Expand All @@ -41,6 +47,32 @@ export async function getTaggedRepos(tag: RepoTopic, page = 1, BLACKLIST:string[
return filteredResults;
}

// Workaround for not spamming console with 404s
const script = `
self.addEventListener('message', async (event) => {
const url = event.data;
const response = await fetch(url);
const data = await response.json().catch(() => null);
self.postMessage(data);
});
`;
const blob = new Blob([script], { type: "application/javascript" });
const workerURL = URL.createObjectURL(blob);

async function fetchRepoManifest(url: string) {
const worker = new Worker(workerURL);
return new Promise((resolver) => {
const resolve = (data) => {
worker.terminate();
resolver(data);
};

worker.postMessage(url);
worker.addEventListener("message", (event) => resolve(event.data), { once: true });
worker.addEventListener("error", () => resolve(null), { once: true });
});
}

// TODO: add try/catch here?
// TODO: can we add a return type here?
/**
Expand All @@ -51,17 +83,20 @@ export async function getTaggedRepos(tag: RepoTopic, page = 1, BLACKLIST:string[
* @returns The manifest object
*/
async function getRepoManifest(user: string, repo: string, branch: string) {
const sessionStorageItem = window.sessionStorage.getItem(`${user}-${repo}`);
const failedSessionStorageItems = window.sessionStorage.getItem("noManifests");
const key = `${user}-${repo}`;
theRealPadster marked this conversation as resolved.
Show resolved Hide resolved
const sessionStorageItem = window.sessionStorage.getItem(key);
const failedSessionStorageItems = JSON.parse(window.sessionStorage.getItem("noManifests") || "[]");
if (sessionStorageItem) return JSON.parse(sessionStorageItem);

const url = `https://raw.githubusercontent.com/${user}/${repo}/${branch}/manifest.json`;
if (failedSessionStorageItems?.includes(url)) return null;
if (failedSessionStorageItems.includes(url)) return null;

let manifest = await fetchRepoManifest(url);

if (!manifest) return addToSessionStorage([url], "noManifests");
if (!Array.isArray(manifest)) manifest = [manifest];

const manifest = await fetch(url).then(res => res.json()).catch(
() => addToSessionStorage([url], "noManifests"),
);
if (manifest) window.sessionStorage.setItem(`${user}-${repo}`, JSON.stringify(manifest));
addToSessionStorage(manifest, key);

return manifest;
}
Expand All @@ -77,16 +112,12 @@ async function getRepoManifest(user: string, repo: string, branch: string) {
export async function fetchExtensionManifest(contents_url: string, branch: string, stars: number, hideInstalled = false) {
try {
// TODO: use the original search full_name ("theRealPadster/spicetify-hide-podcasts") or something to get the url better?
let manifests;
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
const { user, repo } = regex_result.groups;

manifests = await getRepoManifest(user, repo, branch);

// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
const manifests = await getRepoManifest(user, repo, branch);

// Manifest is initially parsed
const parsedManifests: CardItem[] = manifests.reduce((accum, manifest) => {
Expand Down Expand Up @@ -129,9 +160,7 @@ export async function fetchExtensionManifest(contents_url: string, branch: strin
}, []);

return parsedManifests;
}
catch (err) {
// console.warn(contents_url, err);
} catch {
return null;
}
}
Expand All @@ -146,16 +175,12 @@ export async function fetchExtensionManifest(contents_url: string, branch: strin
*/
export async function fetchThemeManifest(contents_url: string, branch: string, stars: number) {
try {
let manifests;
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
const { user, repo } = regex_result.groups;

manifests = await getRepoManifest(user, repo, branch);

// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
const manifests = await getRepoManifest(user, repo, branch);

// Manifest is initially parsed
// const parsedManifests: ThemeCardItem[] = manifests.reduce((accum, manifest) => {
Expand Down Expand Up @@ -196,9 +221,7 @@ export async function fetchThemeManifest(contents_url: string, branch: string, s
return accum;
}, []);
return parsedManifests;
}
catch (err) {
// console.warn(contents_url, err);
} catch {
return null;
}
}
Expand All @@ -213,16 +236,12 @@ export async function fetchThemeManifest(contents_url: string, branch: string, s
export async function fetchAppManifest(contents_url: string, branch: string, stars: number) {
try {
// TODO: use the original search full_name ("theRealPadster/spicetify-hide-podcasts") or something to get the url better?
let manifests;
const regex_result = contents_url.match(/https:\/\/api\.github\.com\/repos\/(?<user>.+)\/(?<repo>.+)\/contents/);
// TODO: err handling?
if (!regex_result || !regex_result.groups) return null;
const { user, repo } = regex_result.groups;

manifests = await getRepoManifest(user, repo, branch);

// If the manifest returned is not an array, initialize it as one
if (!Array.isArray(manifests)) manifests = [manifests];
const manifests = await getRepoManifest(user, repo, branch);

// Manifest is initially parsed
const parsedManifests: CardItem[] = manifests.reduce((accum, manifest) => {
Expand Down Expand Up @@ -263,9 +282,7 @@ export async function fetchAppManifest(contents_url: string, branch: string, sta
}, []);

return parsedManifests;
}
catch (err) {
// console.warn(contents_url, err);
} catch {
return null;
}
}
Expand Down
27 changes: 17 additions & 10 deletions src/logic/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,11 @@ export const initAlbumArtBasedColor = (scheme: ColourScheme) => {
});
};

export const parseCSS = async (themeData: CardItem, tld = "net") => {
export const parseCSS = async (themeData: CardItem, tld?: string) => {
kyrie25 marked this conversation as resolved.
Show resolved Hide resolved
if (!themeData.cssURL) throw new Error("No CSS URL provided");

tld ||= await getAvailableTLD();

const userCssUrl = isGithubRawUrl(themeData.cssURL)
// TODO: this should probably be the URL stored in localstorage actually (i.e. put this url in localstorage)
? `https://cdn.jsdelivr.${tld}/gh/${themeData.user}/${themeData.repo}@${themeData.branch}/${themeData.manifest.usercss}`
Expand Down Expand Up @@ -479,12 +481,13 @@ export const getParamsFromGithubRaw = (url: string) => {
export function addToSessionStorage(items, key?) {
if (!items) return;
items.forEach((item) => {
if (!key) key = `${items.user}-${items.repo}`;
const itemKey = key || `${item.user}-${item.repo}`;

// If the key already exists, it will append to it instead of overwriting it
const existing = window.sessionStorage.getItem(key);
const existing = window.sessionStorage.getItem(itemKey);
const parsed = existing ? JSON.parse(existing) : [];
parsed.push(item);
window.sessionStorage.setItem(key, JSON.stringify(parsed));
window.sessionStorage.setItem(itemKey, JSON.stringify(parsed));
});
}
export function getInvalidCSS(): string[] {
Expand Down Expand Up @@ -581,11 +584,15 @@ export const addExtensionToSpicetifyConfig = (main?: string) => {

// Make a ping to the jsdelivr CDN to check if the user has an internet connection
export async function getAvailableTLD() {
try {
const response = await fetch("https://cdn.jsdelivr.net", { redirect: "manual" });
if (response.type === "opaqueredirect") return "net";
return "xyz";
} catch (err) {
return "xyz";
const tlds = ["net", "xyz"];

for (const tld of tlds) {
try {
const response = await fetch(`https://cdn.jsdelivr.${tld}`, { redirect: "manual", cache: "no-cache" });
if (response.type === "opaqueredirect") return tld;
} catch (err) {
console.error(err);
continue;
}
}
}
8 changes: 2 additions & 6 deletions src/styles/components/_grid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,8 @@
}

.marketplace-header__left {
position: fixed;
left: 16px;

@media (min-width: 1024px) {
left: 32px;
}
position: absolute;
left: 0;
}

.marketplace-grid {
Expand Down
Loading