switch to async storage for faster loading
All checks were successful
deploy / deploy (push) Successful in 1m55s

This commit is contained in:
Lee 2023-10-23 09:54:26 +01:00
parent 70ed248be7
commit a0aca8c9b1
10 changed files with 118 additions and 89 deletions

6
package-lock.json generated

@ -15,6 +15,7 @@
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"idb-keyval": "^6.2.1",
"next": "13.5.6", "next": "13.5.6",
"next-build-id": "^3.0.0", "next-build-id": "^3.0.0",
"node-fetch-cache": "^3.1.3", "node-fetch-cache": "^3.1.3",
@ -3010,6 +3011,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/idb-keyval": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",

@ -16,6 +16,7 @@
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"idb-keyval": "^6.2.1",
"next": "13.5.6", "next": "13.5.6",
"next-build-id": "^3.0.0", "next-build-id": "^3.0.0",
"node-fetch-cache": "^3.1.3", "node-fetch-cache": "^3.1.3",

@ -1,4 +1,4 @@
import { AppProvider } from "@/components/AppProvider"; import AppProvider from "@/components/AppProvider";
import { ssrSettings } from "@/ssrSettings"; import { ssrSettings } from "@/ssrSettings";
import { Metadata } from "next"; import { Metadata } from "next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";

@ -1,18 +1,48 @@
"use client"; "use client";
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore"; import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
import { useSettingsStore } from "@/store/settingsStore";
type AppProviderProps = { import React from "react";
children: React.ReactNode;
};
export function AppProvider({ children }: AppProviderProps) {
return <>{children}</>;
}
const UPDATE_INTERVAL = 1000 * 60 * 15; // 15 minutes const UPDATE_INTERVAL = 1000 * 60 * 15; // 15 minutes
useScoresaberScoresStore.getState().updatePlayerScores(); export default class AppProvider extends React.Component {
setInterval(() => { _state = {
useScoresaberScoresStore.getState().updatePlayerScores(); mounted: false, // Whether the component has mounted
}, UPDATE_INTERVAL); // Whether the data from the async storage has been loaded
dataLoaded: {
scores: false,
settings: false,
},
};
async componentDidMount(): Promise<void> {
if (this._state.mounted) {
return;
}
this._state.mounted = true;
// Load data from async storage
await useSettingsStore.persist.rehydrate();
await useScoresaberScoresStore.persist.rehydrate();
await useSettingsStore.getState().refreshProfiles();
setInterval(() => {
useSettingsStore.getState().refreshProfiles();
}, UPDATE_INTERVAL);
await useScoresaberScoresStore.getState().updatePlayerScores();
setInterval(() => {
useScoresaberScoresStore.getState().updatePlayerScores();
}, UPDATE_INTERVAL);
}
constructor(props: any) {
super(props);
}
render(): React.ReactNode {
const props: any = this.props;
return <>{props.children}</>;
}
}

@ -8,6 +8,7 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
import { useSettingsStore } from "@/store/settingsStore"; import { useSettingsStore } from "@/store/settingsStore";
import { SortType, SortTypes } from "@/types/SortTypes"; import { SortType, SortTypes } from "@/types/SortTypes";
import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { ScoreSaberAPI } from "@/utils/scoresaber/api";
import useStore from "@/utils/useStore";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -27,6 +28,7 @@ type PlayerPageProps = {
const DEFAULT_SORT_TYPE = SortTypes.top; const DEFAULT_SORT_TYPE = SortTypes.top;
export default function PlayerPage({ id }: PlayerPageProps) { export default function PlayerPage({ id }: PlayerPageProps) {
const settingsStore = useStore(useSettingsStore, (store) => store);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@ -49,8 +51,7 @@ export default function PlayerPage({ id }: PlayerPageProps) {
let sortType: SortType; let sortType: SortType;
const sortTypeString = searchParams.get("sort"); const sortTypeString = searchParams.get("sort");
if (sortTypeString == null) { if (sortTypeString == null) {
sortType = sortType = settingsStore?.lastUsedSortType || DEFAULT_SORT_TYPE;
useSettingsStore.getState().lastUsedSortType || DEFAULT_SORT_TYPE;
} else { } else {
sortType = SortTypes[sortTypeString] || DEFAULT_SORT_TYPE; sortType = SortTypes[sortTypeString] || DEFAULT_SORT_TYPE;
} }

@ -3,6 +3,7 @@ import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
import { useSettingsStore } from "@/store/settingsStore"; import { useSettingsStore } from "@/store/settingsStore";
import { SortType, SortTypes } from "@/types/SortTypes"; import { SortType, SortTypes } from "@/types/SortTypes";
import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { ScoreSaberAPI } from "@/utils/scoresaber/api";
import useStore from "@/utils/useStore";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
@ -27,6 +28,7 @@ type ScoresProps = {
}; };
export default function Scores({ playerData, page, sortType }: ScoresProps) { export default function Scores({ playerData, page, sortType }: ScoresProps) {
const settingsStore = useStore(useSettingsStore, (store) => store);
const playerId = playerData.id; const playerId = playerData.id;
const router = useRouter(); const router = useRouter();
@ -61,9 +63,7 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) {
page: page, page: page,
sortType: sortType, sortType: sortType,
}); });
useSettingsStore.setState({ settingsStore?.setLastUsedSortType(sortType);
lastUsedSortType: sortType,
});
if (page > 1) { if (page > 1) {
router.push( router.push(
@ -80,7 +80,7 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) {
}, },
); );
}, },
[playerId, router, scores], [playerId, router, scores, settingsStore],
); );
useEffect(() => { useEffect(() => {

@ -0,0 +1,19 @@
"use client";
import { del, get, set } from "idb-keyval";
import { StateStorage } from "zustand/middleware";
export const IDBStorage: StateStorage = {
getItem: async (name: string): Promise<string | null> => {
//console.log(name, "has been retrieved");
return (await get(name)) || null;
},
setItem: async (name: string, value: string): Promise<void> => {
//console.log(name, "with value", value, "has been saved");
await set(name, value);
},
removeItem: async (name: string): Promise<void> => {
//console.log(name, "has been deleted");
await del(name);
},
};

@ -6,13 +6,12 @@ import { ScoreSaberAPI } from "@/utils/scoresaber/api";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { create } from "zustand"; import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware"; import { createJSONStorage, persist } from "zustand/middleware";
import { IDBStorage } from "./IndexedDBStorage";
import { useSettingsStore } from "./settingsStore"; import { useSettingsStore } from "./settingsStore";
type Player = { type Player = {
id: string; id: string;
scores: { scores: ScoresaberSmallerPlayerScore[];
scoresaber: ScoresaberSmallerPlayerScore[];
};
}; };
interface ScoreSaberScoresStore { interface ScoreSaberScoresStore {
@ -60,14 +59,14 @@ interface ScoreSaberScoresStore {
/** /**
* Refreshes the player scores and adds any new scores to the local database * Refreshes the player scores and adds any new scores to the local database
*/ */
updatePlayerScores: () => void; updatePlayerScores: () => Promise<void>;
} }
const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes
export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()( export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
persist( persist(
(set) => ({ (set, get) => ({
lastUpdated: 0, lastUpdated: 0,
players: [], players: [],
@ -76,12 +75,12 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
}, },
exists: (playerId: string) => { exists: (playerId: string) => {
const players: Player[] = useScoresaberScoresStore.getState().players; const players: Player[] = get().players;
return players.some((player) => player.id == playerId); return players.some((player) => player.id == playerId);
}, },
get: (playerId: string) => { get: (playerId: string) => {
const players: Player[] = useScoresaberScoresStore.getState().players; const players: Player[] = get().players;
return players.find((player) => player.id == playerId); return players.find((player) => player.id == playerId);
}, },
@ -89,10 +88,10 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
playerId: string, playerId: string,
callback?: (page: number, totalPages: number) => void, callback?: (page: number, totalPages: number) => void,
) => { ) => {
const players = useScoresaberScoresStore.getState().players; const players: Player[] = get().players;
// Check if the player already exists // Check if the player already exists
if (useScoresaberScoresStore.getState().exists(playerId)) { if (get().exists(playerId)) {
return { return {
error: true, error: true,
message: "Player already exists", message: "Player already exists",
@ -146,13 +145,12 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
} }
// Remove scores that are already in the database // Remove scores that are already in the database
const player = useScoresaberScoresStore.getState().get(playerId); const player = get().get(playerId);
if (player) { if (player) {
scores = scores.filter( scores = scores.filter(
(score) => (score) =>
player.scores.scoresaber.findIndex( player.scores.findIndex((s) => s.score.id == score.score.id) ==
(s) => s.score.id == score.score.id, -1,
) == -1,
); );
} }
@ -161,9 +159,7 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
...players, ...players,
{ {
id: playerId, id: playerId,
scores: { scores: scores,
scoresaber: scores,
},
}, },
], ],
}); });
@ -174,7 +170,7 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
}, },
updatePlayerScores: async () => { updatePlayerScores: async () => {
const players = useScoresaberScoresStore.getState().players; const players = get().players;
const friends = useSettingsStore.getState().friends; const friends = useSettingsStore.getState().friends;
let allPlayers = new Array<ScoresaberPlayer>(); let allPlayers = new Array<ScoresaberPlayer>();
@ -188,7 +184,11 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
// add local player and friends if they don't exist // add local player and friends if they don't exist
for (const player of allPlayers) { for (const player of allPlayers) {
if (useScoresaberScoresStore.getState().get(player.id) == undefined) { if (get().lastUpdated == 0) {
set({ lastUpdated: Date.now() });
}
if (get().get(player.id) == undefined) {
toast.info( toast.info(
`${ `${
player.id == localPlayer?.id player.id == localPlayer?.id
@ -196,7 +196,7 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
: `Friend ${player.name} was` : `Friend ${player.name} was`
} missing from the ScoreSaber scores database, adding...`, } missing from the ScoreSaber scores database, adding...`,
); );
await useScoresaberScoresStore.getState().addPlayer(player.id); await get().addPlayer(player.id);
toast.success( toast.success(
`${ `${
player.id == useSettingsStore.getState().player?.id player.id == useSettingsStore.getState().player?.id
@ -209,18 +209,14 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
// Skip if we refreshed the scores recently // Skip if we refreshed the scores recently
const timeUntilRefreshMs = const timeUntilRefreshMs =
UPDATE_INTERVAL - UPDATE_INTERVAL - (Date.now() - get().lastUpdated);
(Date.now() - useScoresaberScoresStore.getState().lastUpdated);
if (timeUntilRefreshMs > 0) { if (timeUntilRefreshMs > 0) {
console.log( console.log(
"Waiting", "Waiting",
timeUntilRefreshMs / 1000, timeUntilRefreshMs / 1000,
"seconds to refresh scores for players", "seconds to refresh scores for players",
); );
setTimeout( setTimeout(() => get().updatePlayerScores(), timeUntilRefreshMs);
() => useScoresaberScoresStore.getState().updatePlayerScores(),
timeUntilRefreshMs,
);
return; return;
} }
@ -229,7 +225,7 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
if (player == undefined) continue; if (player == undefined) continue;
console.log(`Updating scores for ${player.id}...`); console.log(`Updating scores for ${player.id}...`);
let oldScores = player.scores.scoresaber; let oldScores: ScoresaberSmallerPlayerScore[] = player.scores;
// Sort the scores by date (newset to oldest), so we know when to stop searching for new scores // Sort the scores by date (newset to oldest), so we know when to stop searching for new scores
oldScores = oldScores.sort((a, b) => { oldScores = oldScores.sort((a, b) => {
@ -296,13 +292,13 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
let newPlayers = players; let newPlayers = players;
// Remove the player if it already exists // Remove the player if it already exists
newPlayers = newPlayers.filter((playerr) => playerr.id != player.id); newPlayers = newPlayers.filter(
(playerr: Player) => playerr.id != player.id,
);
// Add the player // Add the player
newPlayers.push({ newPlayers.push({
id: player.id, id: player.id,
scores: { scores: oldScores,
scoresaber: oldScores,
},
}); });
if (newScoresCount > 0) { if (newScoresCount > 0) {
@ -318,23 +314,8 @@ export const useScoresaberScoresStore = create<ScoreSaberScoresStore>()(
}), }),
{ {
name: "scoresaberScores", name: "scoresaberScores",
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => IDBStorage),
version: 1, version: 1,
migrate: (state: any, version: number) => {
if (version == 1) {
state.players = state.players.map((player: any) => {
return {
id: player.id,
scores: {
scoresaber: player.scores,
},
};
});
return state;
}
},
}, },
), ),
); );

@ -5,6 +5,7 @@ import { SortType, SortTypes } from "@/types/SortTypes";
import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { ScoreSaberAPI } from "@/utils/scoresaber/api";
import { create } from "zustand"; import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware"; import { createJSONStorage, persist } from "zustand/middleware";
import { IDBStorage } from "./IndexedDBStorage";
interface SettingsStore { interface SettingsStore {
player: ScoresaberPlayer | undefined; player: ScoresaberPlayer | undefined;
@ -19,14 +20,14 @@ interface SettingsStore {
clearFriends: () => void; clearFriends: () => void;
setLastUsedSortType: (sortType: SortType) => void; setLastUsedSortType: (sortType: SortType) => void;
setProfilesLastUpdated: (profilesLastUpdated: number) => void; setProfilesLastUpdated: (profilesLastUpdated: number) => void;
refreshProfiles: () => void; refreshProfiles: () => Promise<void>;
} }
const UPDATE_INTERVAL = 1000 * 60 * 10; // 10 minutes const UPDATE_INTERVAL = 1000 * 60 * 10; // 10 minutes
export const useSettingsStore = create<SettingsStore>()( export const useSettingsStore = create<SettingsStore>()(
persist( persist(
(set) => ({ (set, get) => ({
player: undefined, player: undefined,
lastUsedSortType: SortTypes.top, lastUsedSortType: SortTypes.top,
friends: [], friends: [],
@ -39,7 +40,7 @@ export const useSettingsStore = create<SettingsStore>()(
}, },
async addFriend(friendId: string) { async addFriend(friendId: string) {
const friends = useSettingsStore.getState().friends; const friends = this.friends;
if (friends.some((friend) => friend.id == friendId)) { if (friends.some((friend) => friend.id == friendId)) {
return false; return false;
} }
@ -52,7 +53,7 @@ export const useSettingsStore = create<SettingsStore>()(
}, },
removeFriend: (friendId: string) => { removeFriend: (friendId: string) => {
const friends = useSettingsStore.getState().friends; const friends = get().friends;
set({ friends: friends.filter((friend) => friend.id != friendId) }); set({ friends: friends.filter((friend) => friend.id != friendId) });
return friendId; return friendId;
@ -61,7 +62,7 @@ export const useSettingsStore = create<SettingsStore>()(
clearFriends: () => set({ friends: [] }), clearFriends: () => set({ friends: [] }),
isFriend: (friendId: string) => { isFriend: (friendId: string) => {
const friends: ScoresaberPlayer[] = useSettingsStore.getState().friends; const friends: ScoresaberPlayer[] = get().friends;
return friends.some((friend) => friend.id == friendId); return friends.some((friend) => friend.id == friendId);
}, },
@ -75,8 +76,7 @@ export const useSettingsStore = create<SettingsStore>()(
async refreshProfiles() { async refreshProfiles() {
const timeUntilRefreshMs = const timeUntilRefreshMs =
UPDATE_INTERVAL - UPDATE_INTERVAL - (Date.now() - get().profilesLastUpdated);
(Date.now() - useSettingsStore.getState().profilesLastUpdated);
if (timeUntilRefreshMs > 0) { if (timeUntilRefreshMs > 0) {
console.log( console.log(
"Waiting", "Waiting",
@ -87,7 +87,7 @@ export const useSettingsStore = create<SettingsStore>()(
return; return;
} }
const player = useSettingsStore.getState().player; const player = get().player;
if (player != undefined) { if (player != undefined) {
const newPlayer = await ScoreSaberAPI.fetchPlayerData(player.id); const newPlayer = await ScoreSaberAPI.fetchPlayerData(player.id);
if (newPlayer != undefined && newPlayer != null) { if (newPlayer != undefined && newPlayer != null) {
@ -96,7 +96,7 @@ export const useSettingsStore = create<SettingsStore>()(
} }
} }
const friends = useSettingsStore.getState().friends; const friends = get().friends;
const newFriends = await Promise.all( const newFriends = await Promise.all(
friends.map(async (friend) => { friends.map(async (friend) => {
const newFriend = await ScoreSaberAPI.fetchPlayerData(friend.id); const newFriend = await ScoreSaberAPI.fetchPlayerData(friend.id);
@ -105,21 +105,12 @@ export const useSettingsStore = create<SettingsStore>()(
return newFriend; return newFriend;
}), }),
); );
set({ friends: newFriends }); set({ profilesLastUpdated: Date.now(), friends: newFriends });
useSettingsStore.setState({ profilesLastUpdated: Date.now() });
}, },
}), }),
{ {
name: "settings", name: "settings",
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => IDBStorage),
}, },
), ),
); );
// Refresh profiles every 10 minutes
useSettingsStore.getState().refreshProfiles();
setInterval(
() => useSettingsStore.getState().refreshProfiles(),
UPDATE_INTERVAL,
);

@ -118,7 +118,7 @@ export function calcPpBoundary(playerId: string, expectedPp = 1) {
const rankedScores = useScoresaberScoresStore const rankedScores = useScoresaberScoresStore
.getState() .getState()
.players.find((p) => p.id === playerId) .players.find((p) => p.id === playerId)
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined); ?.scores?.filter((s) => s.score.pp !== undefined);
if (!rankedScores) return null; if (!rankedScores) return null;
const rankedScorePps = rankedScores const rankedScorePps = rankedScores
@ -159,7 +159,7 @@ export function getHighestPpPlay(playerId: string) {
const rankedScores = useScoresaberScoresStore const rankedScores = useScoresaberScoresStore
.getState() .getState()
.players.find((p) => p.id === playerId) .players.find((p) => p.id === playerId)
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined); ?.scores?.filter((s) => s.score.pp !== undefined);
if (!rankedScores) return null; if (!rankedScores) return null;
const rankedScorePps = rankedScores const rankedScorePps = rankedScores
@ -179,7 +179,7 @@ export function getAveragePp(playerId: string, limit: number = 20) {
const rankedScores = useScoresaberScoresStore const rankedScores = useScoresaberScoresStore
.getState() .getState()
.players.find((p) => p.id === playerId) .players.find((p) => p.id === playerId)
?.scores?.scoresaber.filter((s) => s.score.pp !== undefined); ?.scores?.filter((s) => s.score.pp !== undefined);
if (!rankedScores) return null; if (!rankedScores) return null;
const rankedScorePps = rankedScores const rankedScorePps = rankedScores