Database conversion. Please wait...
'; + document.body.innerHTML = "Database conversion. Please wait...
"; for (let key of neededFixes) { await allFixes[key].apply(key); } - document.body.innerHTML = ''; -} \ No newline at end of file + document.body.innerHTML = ""; +}; diff --git a/src/db/repositories-init.js b/src/db/repositories-init.js index a1845e0..272bb0c 100644 --- a/src/db/repositories-init.js +++ b/src/db/repositories-init.js @@ -1,19 +1,29 @@ -import cacheRepository from './repository/cache'; -import groupsRepository from './repository/groups'; -import keyValueRepository from './repository/key-value'; -import playersRepository from './repository/players'; -import playersHistoryRepository from './repository/players-history'; -import rankedsRepository from './repository/rankeds'; -import rankedsChangesRepository from './repository/rankeds-changes'; -import scoresRepository from './repository/scores'; -import songsRepository from './repository/songs'; -import twitchRepository from './repository/twitch'; -import log from '../utils/logger'; - +import cacheRepository from "./repository/cache"; +import groupsRepository from "./repository/groups"; +import keyValueRepository from "./repository/key-value"; +import playersRepository from "./repository/players"; +import playersHistoryRepository from "./repository/players-history"; +import rankedsRepository from "./repository/rankeds"; +import rankedsChangesRepository from "./repository/rankeds-changes"; +import scoresRepository from "./repository/scores"; +import songsRepository from "./repository/songs"; +import twitchRepository from "./repository/twitch"; +import log from "../utils/logger"; export default () => { - log.debug('Initialize DB repositories'); + log.debug("Initialize DB repositories"); // initialize all repositories in order to create cache to sync - [cacheRepository, groupsRepository, keyValueRepository, playersRepository, playersHistoryRepository, rankedsRepository, rankedsChangesRepository, scoresRepository, songsRepository, twitchRepository].map(repository => repository()); -} \ No newline at end of file + [ + cacheRepository, + groupsRepository, + keyValueRepository, + playersRepository, + playersHistoryRepository, + rankedsRepository, + rankedsChangesRepository, + scoresRepository, + songsRepository, + twitchRepository, + ].map((repository) => repository()); +}; diff --git a/src/db/repository/accsaber-categories.js b/src/db/repository/accsaber-categories.js index c82a759..6ef3ffb 100644 --- a/src/db/repository/accsaber-categories.js +++ b/src/db/repository/accsaber-categories.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('accsaber-categories', 'name'); \ No newline at end of file +export default () => createRepository("accsaber-categories", "name"); diff --git a/src/db/repository/accsaber-players-history.js b/src/db/repository/accsaber-players-history.js index 388ee7c..9fb0225 100644 --- a/src/db/repository/accsaber-players-history.js +++ b/src/db/repository/accsaber-players-history.js @@ -1,6 +1,7 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('accsaber-players-history', 'playerIdTimestamp', { - 'accsaber-players-history-playerId': 'playerId', - 'accsaber-players-history-playerIdTimestamp': 'playerIdTimestamp' -}); \ No newline at end of file +export default () => + createRepository("accsaber-players-history", "playerIdTimestamp", { + "accsaber-players-history-playerId": "playerId", + "accsaber-players-history-playerIdTimestamp": "playerIdTimestamp", + }); diff --git a/src/db/repository/accsaber-players.js b/src/db/repository/accsaber-players.js index aeddc98..b0b580a 100644 --- a/src/db/repository/accsaber-players.js +++ b/src/db/repository/accsaber-players.js @@ -1,10 +1,7 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository( - 'accsaber-players', - 'id', - { - 'accsaber-players-playerId': 'playerId', - 'accsaber-players-category': 'category', - }, -); \ No newline at end of file +export default () => + createRepository("accsaber-players", "id", { + "accsaber-players-playerId": "playerId", + "accsaber-players-category": "category", + }); diff --git a/src/db/repository/beat-savior-files.js b/src/db/repository/beat-savior-files.js index 5ca6c53..b98707b 100644 --- a/src/db/repository/beat-savior-files.js +++ b/src/db/repository/beat-savior-files.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('beat-savior-files', 'fileId'); \ No newline at end of file +export default () => createRepository("beat-savior-files", "fileId"); diff --git a/src/db/repository/beat-savior-players.js b/src/db/repository/beat-savior-players.js index df41cce..ff520bd 100644 --- a/src/db/repository/beat-savior-players.js +++ b/src/db/repository/beat-savior-players.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('beat-savior-players', 'playerId'); \ No newline at end of file +export default () => createRepository("beat-savior-players", "playerId"); diff --git a/src/db/repository/beat-savior.js b/src/db/repository/beat-savior.js index f765934..d4f14af 100644 --- a/src/db/repository/beat-savior.js +++ b/src/db/repository/beat-savior.js @@ -1,6 +1,7 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('beat-savior', 'beatSaviorId', { - 'beat-savior-playerId': 'playerId', - 'beat-savior-hash': 'hash', -}); \ No newline at end of file +export default () => + createRepository("beat-savior", "beatSaviorId", { + "beat-savior-playerId": "playerId", + "beat-savior-hash": "hash", + }); diff --git a/src/db/repository/cache.js b/src/db/repository/cache.js index 96f8e2c..6cf12ca 100644 --- a/src/db/repository/cache.js +++ b/src/db/repository/cache.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('cache'); \ No newline at end of file +export default () => createRepository("cache"); diff --git a/src/db/repository/generic.js b/src/db/repository/generic.js index efe276a..c3c1067 100644 --- a/src/db/repository/generic.js +++ b/src/db/repository/generic.js @@ -1,11 +1,11 @@ -import cache from '../cache'; -import {db} from '../db'; -import {convertArrayToObjectByKey} from '../../utils/js' -import makePendingPromisePool from '../../utils/pending-promises' -import eventBus from '../../utils/broadcast-channel-pubsub' +import cache from "../cache"; +import { db } from "../db"; +import { convertArrayToObjectByKey } from "../../utils/js"; +import makePendingPromisePool from "../../utils/pending-promises"; +import eventBus from "../../utils/broadcast-channel-pubsub"; -export const ALL_KEY = '__ALL'; -const NONE_KEY = '__NONE'; +export const ALL_KEY = "__ALL"; +const NONE_KEY = "__NONE"; let repositories = {}; @@ -20,46 +20,52 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => { const getKeyName = () => inlineKeyName; const hasOutOfLineKey = () => getKeyName() === undefined; const getObjKey = (obj, outOfLineKey = undefined) => { - const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName] + const key = hasOutOfLineKey() ? outOfLineKey : obj[inlineKeyName]; return key ? key : outOfLineKey; - } + }; let repositoryCache = cache(repositoryName, getObjKey); - const getCacheKeyFor = (query, indexName) => (indexName ? indexName : ALL_KEY) + '-' + (query ? query : NONE_KEY); + const getCacheKeyFor = (query, indexName) => + (indexName ? indexName : ALL_KEY) + "-" + (query ? query : NONE_KEY); - const getFieldForIndexName = indexName => indexesKeyNames[indexName]; - const isFieldForIndexDefined = indexName => !!getFieldForIndexName(indexName); + const getFieldForIndexName = (indexName) => indexesKeyNames[indexName]; + const isFieldForIndexDefined = (indexName) => + !!getFieldForIndexName(indexName); - const setDataAvailabilityStatus = cacheKey => dataAvailableFor[cacheKey] = true; - const setAllDataAvailabilityStatus = () => setDataAvailabilityStatus(getCacheKeyFor()); - const removeDataAvailabilityStatus = cacheKey => { + const setDataAvailabilityStatus = (cacheKey) => + (dataAvailableFor[cacheKey] = true); + const setAllDataAvailabilityStatus = () => + setDataAvailabilityStatus(getCacheKeyFor()); + const removeDataAvailabilityStatus = (cacheKey) => { delete dataAvailableFor[cacheKey]; delete dataAvailableFor[getCacheKeyFor()]; - } - const flushDataAvailabilityStatus = () => dataAvailableFor = {}; - const isIndexDataAvailable = cacheKey => !!dataAvailableFor[cacheKey]; + }; + const flushDataAvailabilityStatus = () => (dataAvailableFor = {}); + const isIndexDataAvailable = (cacheKey) => !!dataAvailableFor[cacheKey]; const isAllDataAvailable = () => isIndexDataAvailable(getCacheKeyFor()); const flushCache = () => { repositoryCache.flush(); flushDataAvailabilityStatus(); - } + }; - const forgetCacheKey = key => repositoryCache.forget(key); + const forgetCacheKey = (key) => repositoryCache.forget(key); - const forgetObject = async obj => { - if (hasOutOfLineKey()) throw 'forgetObject function is not available in repositories with out-of-line keys'; + const forgetObject = async (obj) => { + if (hasOutOfLineKey()) + throw "forgetObject function is not available in repositories with out-of-line keys"; const key = getObjKey(obj); - if (!key) throw `Object does not contain ${inlineKeyName} field which is repository key`; + if (!key) + throw `Object does not contain ${inlineKeyName} field which is repository key`; forgetCacheKey(key); - } + }; const getStoreName = () => storeName; - const getCachedKeys = _ => repositoryCache.getKeys(); + const getCachedKeys = (_) => repositoryCache.getKeys(); const getAllKeys = async () => db.getAllKeys(storeName); @@ -68,16 +74,23 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => { const cacheKey = getCacheKeyFor(key); - return repositoryCache.get(key, () => resolvePromiseOrWaitForPending(cacheKey, () => db.get(storeName, key))); + return repositoryCache.get(key, () => + resolvePromiseOrWaitForPending(cacheKey, () => db.get(storeName, key)), + ); }; const getFromIndex = async (indexName, query, refreshCache = false) => { - if (hasOutOfLineKey()) throw `getFromIndex() is not available for stores with out-of-line key`; - if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`; + if (hasOutOfLineKey()) + throw `getFromIndex() is not available for stores with out-of-line key`; + if (!isFieldForIndexDefined(indexName)) + throw `Index ${indexName} has no field set`; - const cacheKey = getCacheKeyFor(query, indexName + '-single'); + const cacheKey = getCacheKeyFor(query, indexName + "-single"); - const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getFromIndex(storeName, indexName, query)); + const getFromDb = () => + resolvePromiseOrWaitForPending(cacheKey, () => + db.getFromIndex(storeName, indexName, query), + ); if (query && query instanceof IDBKeyRange) return getFromDb(); @@ -85,7 +98,8 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => { const fullIndexCacheKey = getCacheKeyFor(query, indexName); - const filterItems = item => item !== undefined && (!query || item[field] === query); + const filterItems = (item) => + item !== undefined && (!query || item[field] === query); if (refreshCache) { removeDataAvailabilityStatus(cacheKey); @@ -94,24 +108,34 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => { repositoryCache.forgetByFilter(filterItems); } - return repositoryCache.getByFilter(getFromDb, isAllDataAvailable() || isIndexDataAvailable(cacheKey) || isIndexDataAvailable(fullIndexCacheKey) ? filterItems : null); + return repositoryCache.getByFilter( + getFromDb, + isAllDataAvailable() || + isIndexDataAvailable(cacheKey) || + isIndexDataAvailable(fullIndexCacheKey) + ? filterItems + : null, + ); }; - const getAll = async(refreshCache = false) => { + const getAll = async (refreshCache = false) => { const cacheKey = getCacheKeyFor(); - const getFromDb = () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName)) + const getFromDb = () => + resolvePromiseOrWaitForPending(cacheKey, () => db.getAll(storeName)); if (hasOutOfLineKey()) return getFromDb(); if (refreshCache) flushCache(); - const filterUndefined = item => item !== undefined; + const filterUndefined = (item) => item !== undefined; if (!isAllDataAvailable()) { const data = convertArrayToObjectByKey(await getFromDb(), inlineKeyName); - const ret = Object.values(repositoryCache.setAll(data)).filter(filterUndefined); + const ret = Object.values(repositoryCache.setAll(data)).filter( + filterUndefined, + ); setAllDataAvailabilityStatus(); @@ -119,92 +143,111 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => { } return Object.values(repositoryCache.getAll()).filter(filterUndefined); - } + }; - const getAllFromIndex = async(indexName, query = undefined, refreshCache = false) => { - if (hasOutOfLineKey()) throw `getAllFromIndex() is not available for stores with out-of-line key`; - if (!isFieldForIndexDefined(indexName)) throw `Index ${indexName} has no field set`; + const getAllFromIndex = async ( + indexName, + query = undefined, + refreshCache = false, + ) => { + if (hasOutOfLineKey()) + throw `getAllFromIndex() is not available for stores with out-of-line key`; + if (!isFieldForIndexDefined(indexName)) + throw `Index ${indexName} has no field set`; const cacheKey = getCacheKeyFor(query, indexName); - const getFromDb = async () => resolvePromiseOrWaitForPending(cacheKey, () => db.getAllFromIndex(storeName, indexName, query)); + const getFromDb = async () => + resolvePromiseOrWaitForPending(cacheKey, () => + db.getAllFromIndex(storeName, indexName, query), + ); if (query && query instanceof IDBKeyRange) return getFromDb(); const field = getFieldForIndexName(indexName); - const filterItems = item => item !== undefined && (!query || item[field] === query); + const filterItems = (item) => + item !== undefined && (!query || item[field] === query); if (refreshCache) { removeDataAvailabilityStatus(cacheKey); repositoryCache.forgetByFilter(filterItems); } - const getFromDbAndUpdateCache = async () => resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => { - const data = await getFromDb(); + const getFromDbAndUpdateCache = async () => + resolvePromiseOrWaitForPending(`${cacheKey}-updateDb`, async () => { + const data = await getFromDb(); - repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName)); + repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName)); - setDataAvailabilityStatus(cacheKey); + setDataAvailabilityStatus(cacheKey); - return data; - }) + return data; + }); - if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey)) return await getFromDbAndUpdateCache(); + if (!isAllDataAvailable() && !isIndexDataAvailable(cacheKey)) + return await getFromDbAndUpdateCache(); return Object.values(repositoryCache.getAll()).filter(filterItems); - } + }; const set = async (value, key = undefined, tx = null) => { const txStores = tx ? [...tx.objectStoreNames] : null; let putKey; if (tx && txStores.includes(storeName)) { - putKey = await tx.objectStore(storeName).put(value, inlineKeyName ? undefined : key); + putKey = await tx + .objectStore(storeName) + .put(value, inlineKeyName ? undefined : key); } else { - putKey = await db.put(storeName, value, inlineKeyName ? undefined : key) + putKey = await db.put(storeName, value, inlineKeyName ? undefined : key); } if (!hasOutOfLineKey() && !getObjKey(value)) value[inlineKeyName] = putKey; return repositoryCache.set(getObjKey(value, key), value); - } + }; - const del = async key => { + const del = async (key) => { await db.delete(storeName, key); return repositoryCache.forget(key); - } + }; - const deleteObject = async obj => { - if (hasOutOfLineKey()) throw 'deleteObject function is not available in repositories with out-of-line keys'; + const deleteObject = async (obj) => { + if (hasOutOfLineKey()) + throw "deleteObject function is not available in repositories with out-of-line keys"; const key = getObjKey(obj); - if (!key) throw `Object does not contain ${inlineKeyName} field which is repository key`; + if (!key) + throw `Object does not contain ${inlineKeyName} field which is repository key`; return del(key); - } + }; - const openCursor = async (mode = 'readonly') => db.transaction(storeName, mode).store.openCursor(); + const openCursor = async (mode = "readonly") => + db.transaction(storeName, mode).store.openCursor(); const setCache = (value, key) => { if (hasOutOfLineKey()) { - if (!key) throw `setCache() needs a key for stores (${storeName}) with out-of-line keys`; + if (!key) + throw `setCache() needs a key for stores (${storeName}) with out-of-line keys`; } else { key = getObjKey(value, key); } repositoryCache.set(key, value); - } - const addToCache = data => { - if (hasOutOfLineKey()) throw `addToCache() is not available for stores (${storeName}) with out-of-line key`; + }; + const addToCache = (data) => { + if (hasOutOfLineKey()) + throw `addToCache() is not available for stores (${storeName}) with out-of-line key`; repositoryCache.merge(convertArrayToObjectByKey(data, inlineKeyName)); - } + }; const getCache = () => repositoryCache; - return repositories[repositoryName] = { + return (repositories[repositoryName] = { getStoreName, hasOutOfLineKey, getAllKeys, @@ -224,5 +267,5 @@ export default (storeName, inlineKeyName = undefined, indexesKeyNames = {}) => { setCache, addToCache, getCache, - }; + }); }; diff --git a/src/db/repository/groups.js b/src/db/repository/groups.js index c01c7ea..9b0ca93 100644 --- a/src/db/repository/groups.js +++ b/src/db/repository/groups.js @@ -1,5 +1,11 @@ -import createRepository from './generic'; +import createRepository from "./generic"; let repository; -export default () => repository ? repository : repository = createRepository('groups', '_idbId', {'groups-name': 'name', 'groups-playerId': 'playerId'}); \ No newline at end of file +export default () => + repository + ? repository + : (repository = createRepository("groups", "_idbId", { + "groups-name": "name", + "groups-playerId": "playerId", + })); diff --git a/src/db/repository/key-value.js b/src/db/repository/key-value.js index b129d4b..91861cf 100644 --- a/src/db/repository/key-value.js +++ b/src/db/repository/key-value.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('key-value'); \ No newline at end of file +export default () => createRepository("key-value"); diff --git a/src/db/repository/players-history.js b/src/db/repository/players-history.js index 6d1a152..8b5fc3a 100644 --- a/src/db/repository/players-history.js +++ b/src/db/repository/players-history.js @@ -1,6 +1,7 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('players-history', '_idbId', { - 'players-history-playerId': 'playerId', - 'players-history-playerIdSsTimestamp': 'playerIdSsTimestamp' -}); \ No newline at end of file +export default () => + createRepository("players-history", "_idbId", { + "players-history-playerId": "playerId", + "players-history-playerIdSsTimestamp": "playerIdSsTimestamp", + }); diff --git a/src/db/repository/players.js b/src/db/repository/players.js index da418f7..94175e7 100644 --- a/src/db/repository/players.js +++ b/src/db/repository/players.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('players', 'playerId'); \ No newline at end of file +export default () => createRepository("players", "playerId"); diff --git a/src/db/repository/rankeds-changes.js b/src/db/repository/rankeds-changes.js index cae9b96..5d755c5 100644 --- a/src/db/repository/rankeds-changes.js +++ b/src/db/repository/rankeds-changes.js @@ -1,3 +1,7 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('rankeds-changes', '_idbId', {'rankeds-changes-timestamp': 'timestamp', 'rankeds-changes-leaderboardId': 'leaderboardId'}); \ No newline at end of file +export default () => + createRepository("rankeds-changes", "_idbId", { + "rankeds-changes-timestamp": "timestamp", + "rankeds-changes-leaderboardId": "leaderboardId", + }); diff --git a/src/db/repository/rankeds.js b/src/db/repository/rankeds.js index b5cbba7..92806f7 100644 --- a/src/db/repository/rankeds.js +++ b/src/db/repository/rankeds.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('rankeds', 'leaderboardId'); \ No newline at end of file +export default () => createRepository("rankeds", "leaderboardId"); diff --git a/src/db/repository/scores-update-queue.js b/src/db/repository/scores-update-queue.js index a33794c..a7ec833 100644 --- a/src/db/repository/scores-update-queue.js +++ b/src/db/repository/scores-update-queue.js @@ -1,9 +1,6 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository( - 'scores-update-queue', - 'id', - { - 'scores-update-queue-fetchedAt': 'fetchedAt', - }, - ) \ No newline at end of file +export default () => + createRepository("scores-update-queue", "id", { + "scores-update-queue-fetchedAt": "fetchedAt", + }); diff --git a/src/db/repository/scores.js b/src/db/repository/scores.js index 1a1c83d..68f4a9c 100644 --- a/src/db/repository/scores.js +++ b/src/db/repository/scores.js @@ -1,12 +1,9 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository( - 'scores', - 'id', - { - 'scores-timeset': 'timeset', - 'scores-leaderboardId': 'leaderboardId', - 'scores-playerId': 'playerId', - 'scores-pp': 'pp', - }, - ) \ No newline at end of file +export default () => + createRepository("scores", "id", { + "scores-timeset": "timeset", + "scores-leaderboardId": "leaderboardId", + "scores-playerId": "playerId", + "scores-pp": "pp", + }); diff --git a/src/db/repository/songs-beatmaps.js b/src/db/repository/songs-beatmaps.js index 75fad2b..17cc4cc 100644 --- a/src/db/repository/songs-beatmaps.js +++ b/src/db/repository/songs-beatmaps.js @@ -1,3 +1,4 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('songs-beatmaps', 'hash', {'songs-beatmaps-key': 'key'}); \ No newline at end of file +export default () => + createRepository("songs-beatmaps", "hash", { "songs-beatmaps-key": "key" }); diff --git a/src/db/repository/songs.js b/src/db/repository/songs.js index 0e8b9f0..47b5656 100644 --- a/src/db/repository/songs.js +++ b/src/db/repository/songs.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('songs', 'hash', {'songs-key': 'key'}); \ No newline at end of file +export default () => createRepository("songs", "hash", { "songs-key": "key" }); diff --git a/src/db/repository/twitch.js b/src/db/repository/twitch.js index 9d98845..9b63dcb 100644 --- a/src/db/repository/twitch.js +++ b/src/db/repository/twitch.js @@ -1,3 +1,3 @@ -import createRepository from './generic'; +import createRepository from "./generic"; -export default () => createRepository('twitch', 'playerId'); \ No newline at end of file +export default () => createRepository("twitch", "playerId"); diff --git a/src/main.js b/src/main.js index 110932e..7e1d6e7 100644 --- a/src/main.js +++ b/src/main.js @@ -1,35 +1,35 @@ -import App from './App.svelte'; -import log from './utils/logger' -import initDb from './db/db' -import initializeRepositories from './db/repositories-init'; -import setupDataFixes from './db/fix-data' -import createConfigStore from './stores/config' -import createPlayerService from './services/scoresaber/player' -import createBeatSaviorService from './services/beatsavior' -import createRankedsStore from './stores/scoresaber/rankeds' -import initDownloadManager from './network/download-manager' -import initCommandProcessor from './network/command-processor' -import {enablePatches, setAutoFreeze} from 'immer' -import {initCompareEnhancer} from './stores/http/enhancers/scores/compare' -import ErrorComponent from './components/Common/Error.svelte' -import initializeWorkers from './utils/worker-wrappers' +import App from "./App.svelte"; +import log from "./utils/logger"; +import initDb from "./db/db"; +import initializeRepositories from "./db/repositories-init"; +import setupDataFixes from "./db/fix-data"; +import createConfigStore from "./stores/config"; +import createPlayerService from "./services/scoresaber/player"; +import createBeatSaviorService from "./services/beatsavior"; +import createRankedsStore from "./stores/scoresaber/rankeds"; +import initDownloadManager from "./network/download-manager"; +import initCommandProcessor from "./network/command-processor"; +import { enablePatches, setAutoFreeze } from "immer"; +import { initCompareEnhancer } from "./stores/http/enhancers/scores/compare"; +import ErrorComponent from "./components/Common/Error.svelte"; +import initializeWorkers from "./utils/worker-wrappers"; let app = null; -(async() => { +(async () => { try { // TODO: remove level setting // log.setLevel(log.TRACE); // log.logOnly(['AccSaberService']); - log.info('Starting up...', 'Main') + log.info("Starting up...", "Main"); await initDb(); await initializeRepositories(); await setupDataFixes(); // WORKAROUND for immer.js esm (see https://github.com/immerjs/immer/issues/557) - window.process = {env: {NODE_ENV: "production"}}; + window.process = { env: { NODE_ENV: "production" } }; // setup immer.js enablePatches(); @@ -47,24 +47,29 @@ let app = null; initCommandProcessor(await initDownloadManager()); - log.info('Site initialized', 'Main') + log.info("Site initialized", "Main"); app = new App({ target: document.body, props: {}, }); - } catch(error) { + } catch (error) { console.error(error); - if (error instanceof DOMException && error.toString() === 'InvalidStateError: A mutation operation was attempted on a database that did not allow mutations.') - error = new Error('Firefox in private mode does not support the database. Please run the site in normal mode.') + if ( + error instanceof DOMException && + error.toString() === + "InvalidStateError: A mutation operation was attempted on a database that did not allow mutations." + ) + error = new Error( + "Firefox in private mode does not support the database. Please run the site in normal mode.", + ); app = new ErrorComponent({ target: document.body, - props: {error, withTrace: true}, + props: { error, withTrace: true }, }); } })(); - -export default app; \ No newline at end of file +export default app; diff --git a/src/network/cache.js b/src/network/cache.js index f82a863..5b98d44 100644 --- a/src/network/cache.js +++ b/src/network/cache.js @@ -1,5 +1,5 @@ // import eventBus from '../utils/broadcast-channel-pubsub' -import {addToDate, MINUTE} from '../utils/date' +import { addToDate, MINUTE } from "../utils/date"; const DEFAULT_CACHE_SIZE = 100; @@ -7,20 +7,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { let cache = {}; let cacheSize = size; - const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope + const isWorker = + typeof WorkerGlobalScope !== "undefined" && + self instanceof WorkerGlobalScope; const defaultExpiryIn = expiryIn; - const packValue = value => { - if (!value || typeof value !== 'object') return value; + const packValue = (value) => { + if (!value || typeof value !== "object") return value; - const newValue = {...value}; + const newValue = { ...value }; if (value.headers && value.headers instanceof Headers) { - newValue.headers = [...value.headers.entries()].reduce((cum, [key, value]) => { - cum[key] = value; - return cum; - }, {}) + newValue.headers = [...value.headers.entries()].reduce( + (cum, [key, value]) => { + cum[key] = value; + return cum; + }, + {}, + ); } if (value.body && value.body instanceof Document) { @@ -28,25 +33,29 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { } return newValue; - } + }; - const unpackValue = value => { - if (!value || typeof value !== 'object') return value; + const unpackValue = (value) => { + if (!value || typeof value !== "object") return value; - const newValue = {...value}; + const newValue = { ...value }; if (value.headers) { const headers = new Headers(); - Object.keys(value.headers).map(k => headers.append(k, value.headers[k])); + Object.keys(value.headers).map((k) => + headers.append(k, value.headers[k]), + ); newValue.headers = headers; } if (value.body) { - newValue.body = !isWorker ? new DOMParser().parseFromString(value.body, 'text/html') : value.body; + newValue.body = !isWorker + ? new DOMParser().parseFromString(value.body, "text/html") + : value.body; } return newValue; - } + }; // update data cached on another node // const setUnsubscribe = eventBus.on('net-cache-key-set', ({key, value, expiryIn}, isLocal) => !isLocal ? set(key, unpackValue(value), expiryIn, false) : null); @@ -54,14 +63,25 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { // const flushUnsubscribe = eventBus.on('net-cache-flush', (_, isLocal) => !isLocal ? flush(false) : null); const has = (key, maxAge = null, withExpired = false) => - cache.hasOwnProperty(key) && cache[key] && - (withExpired || !cache[key].expiryAt || cache[key].expiryAt >= new Date()) && - (!Number.isFinite(maxAge) || !cache[key].cachedAt || addToDate(maxAge, cache[key].cachedAt) >= new Date()); + cache.hasOwnProperty(key) && + cache[key] && + (withExpired || + !cache[key].expiryAt || + cache[key].expiryAt >= new Date()) && + (!Number.isFinite(maxAge) || + !cache[key].cachedAt || + addToDate(maxAge, cache[key].cachedAt) >= new Date()); const set = (key, value, expiryIn = null, emitEvent = true) => { expiryIn = expiryIn ? expiryIn : defaultExpiryIn; - cache[key] = {key, cachedAt: new Date(), expiryIn, expiryAt: addToDate(expiryIn, new Date()), value}; + cache[key] = { + key, + cachedAt: new Date(), + expiryIn, + expiryAt: addToDate(expiryIn, new Date()), + value, + }; // if (emitEvent) eventBus.publish('net-cache-key-set', {key, value: packValue(value), expiryIn}); @@ -70,7 +90,12 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { return value; }; - const get = (key, maxAge = null, withExpired = false, valueOnly = true) => has(key, maxAge, withExpired) ? (valueOnly ? cache[key].value : cache[key]) : undefined; + const get = (key, maxAge = null, withExpired = false, valueOnly = true) => + has(key, maxAge, withExpired) + ? valueOnly + ? cache[key].value + : cache[key] + : undefined; const getAll = () => cache; @@ -82,7 +107,7 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { // if (emitEvent) eventBus.publish('net-cache-key-forget', {key}); return cache; - } + }; const flush = (emitEvent = true) => { cache = {}; @@ -90,23 +115,26 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { // if (emitEvent) eventBus.publish('net-cache-flush', {}); return cache; - } + }; const garbageCollect = (size = cacheSize) => { const values = Object.values(cache); if (values.length < size) return; cache = values - .sort((a,b) => b.expiryAt - a.expiryAt) + .sort((a, b) => b.expiryAt - a.expiryAt) .slice(0, size) - .reduce((cum, item) => {cum[item.key] = item; return cum;}, {}); - } + .reduce((cum, item) => { + cum[item.key] = item; + return cum; + }, {}); + }; const destroy = () => { // setUnsubscribe(); // forgetUnsubscribe(); // flushUnsubscribe(); - } + }; return { has, @@ -117,5 +145,5 @@ export default (size = DEFAULT_CACHE_SIZE, expiryIn = MINUTE) => { forget, flush, destroy, - } -} + }; +}; diff --git a/src/network/clients/accsaber/api-categories.js b/src/network/clients/accsaber/api-categories.js index 2f786cd..bff4fd5 100644 --- a/src/network/clients/accsaber/api-categories.js +++ b/src/network/clients/accsaber/api-categories.js @@ -1,19 +1,22 @@ -import queue from '../../queues/queues' -import createClient from '../generic' +import queue from "../../queues/queues"; +import createClient from "../generic"; -const process = response => { +const process = (response) => { if (!response || !Array.isArray(response)) return []; - return response.map(c => ({ + return response.map((c) => ({ name: c.categoryName, displayName: c.categoryDisplayName, countsTowardsOverall: c.countsTowardsOverall, - description: c.description + description: c.description, })); -} +}; -const get = async ({priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.ACCSABER.categories(priority, queueOptions); +const get = async ({ + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.ACCSABER.categories(priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/accsaber/api-leaderboard.js b/src/network/clients/accsaber/api-leaderboard.js index e70c884..45c8896 100644 --- a/src/network/clients/accsaber/api-leaderboard.js +++ b/src/network/clients/accsaber/api-leaderboard.js @@ -1,10 +1,16 @@ -import queue from '../../queues/queues' -import createClient from '../generic' -import {dateFromString, formatDateRelative} from '../../../utils/date' -import {LEADERBOARD_SCORES_PER_PAGE} from '../../../utils/accsaber/consts' +import queue from "../../queues/queues"; +import createClient from "../generic"; +import { dateFromString, formatDateRelative } from "../../../utils/date"; +import { LEADERBOARD_SCORES_PER_PAGE } from "../../../utils/accsaber/consts"; -const process = response => { - if (!response || !Array.isArray(response.responses) || response.responses.length !== 2 || !Array.isArray(response.responses[0])) return []; +const process = (response) => { + if ( + !response || + !Array.isArray(response.responses) || + response.responses.length !== 2 || + !Array.isArray(response.responses[0]) + ) + return []; const page = response?.fetchOptions.page ?? 1; const totalItems = response.responses[0].length; @@ -25,16 +31,32 @@ const process = response => { difficulty, } = mapInfo; - const song = {hash, name, subName, authorName, levelAuthorName, beatsaverKey}; - const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')} - const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName}; + const song = { + hash, + name, + subName, + authorName, + levelAuthorName, + beatsaverKey, + }; + const diffInfo = { + type: "Standard", + diff: difficulty?.toLowerCase()?.replace("plus", "Plus"), + }; + const leaderboard = { + leaderboardId, + song, + diffInfo, + complexity, + categoryDisplayName, + }; return { page, pageQty, totalItems, leaderboard, - scores: response.responses[0].map(s => { + scores: response.responses[0].map((s) => { let { accuracy: acc, ap, @@ -48,14 +70,16 @@ const process = response => { if (acc && Number.isFinite(acc)) acc *= 100; - timeSet = dateFromString(timeSet) + timeSet = dateFromString(timeSet); const timeSetString = formatDateRelative(timeSet); return { player: { name, playerId, - playerInfo: {avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`}, + playerInfo: { + avatar: `https://cdn.accsaber.com/avatars/${playerId}.jpg`, + }, }, score: { acc, @@ -67,20 +91,31 @@ const process = response => { timeSetString, }, other: rest, - } + }; }), }; -} +}; -const get = async ({leaderboardId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => { +const get = async ({ + leaderboardId, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => { const responses = await Promise.all([ queue.ACCSABER.leaderboard(leaderboardId, page, priority, queueOptions), - queue.ACCSABER.leaderboardInfo(leaderboardId, priority, queueOptions) + queue.ACCSABER.leaderboardInfo(leaderboardId, priority, queueOptions), ]); - return {...responses[0], body: {responses: responses.map(r => r.body), fetchOptions: {leaderboardId, page}}} -} + return { + ...responses[0], + body: { + responses: responses.map((r) => r.body), + fetchOptions: { leaderboardId, page }, + }, + }; +}; const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/accsaber/api-player-rank-history.js b/src/network/clients/accsaber/api-player-rank-history.js index b3627de..15e507f 100644 --- a/src/network/clients/accsaber/api-player-rank-history.js +++ b/src/network/clients/accsaber/api-player-rank-history.js @@ -1,29 +1,44 @@ -import queue from '../../queues/queues' -import createClient from '../generic' -import {fromAccSaberDateString} from '../../../utils/date' -import {isDateObject} from '../../../utils/js' +import queue from "../../queues/queues"; +import createClient from "../generic"; +import { fromAccSaberDateString } from "../../../utils/date"; +import { isDateObject } from "../../../utils/js"; -const process = response => { +const process = (response) => { const playerId = response?.fetchOptions?.playerId ?? null; - if (!response?.response || !Object.keys(response.response)?.length || !playerId) return []; - + if ( + !response?.response || + !Object.keys(response.response)?.length || + !playerId + ) + return []; return { playerId, history: Object.entries(response.response) - .map(([date, rank]) => ({date: fromAccSaberDateString(date), rank})) - .filter(obj => isDateObject(obj?.date)) - .sort((a,b) => a.date.getTime() - b.date.getTime()) - , - } -} + .map(([date, rank]) => ({ date: fromAccSaberDateString(date), rank })) + .filter((obj) => isDateObject(obj?.date)) + .sort((a, b) => a.date.getTime() - b.date.getTime()), + }; +}; -const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => { - const response = await queue.ACCSABER.playerRankHistory(playerId, priority, queueOptions); +const get = async ({ + playerId, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => { + const response = await queue.ACCSABER.playerRankHistory( + playerId, + priority, + queueOptions, + ); - return {...response, body: {response: response.body, fetchOptions: {playerId}}} -} + return { + ...response, + body: { response: response.body, fetchOptions: { playerId } }, + }; +}; const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/accsaber/api-ranking.js b/src/network/clients/accsaber/api-ranking.js index 1522db9..1978bd0 100644 --- a/src/network/clients/accsaber/api-ranking.js +++ b/src/network/clients/accsaber/api-ranking.js @@ -1,24 +1,37 @@ -import queue from '../../queues/queues' -import createClient from '../generic' +import queue from "../../queues/queues"; +import createClient from "../generic"; -const process = response => { - const category = response?.fetchOptions?.category ?? 'overall'; +const process = (response) => { + const category = response?.fetchOptions?.category ?? "overall"; if (!response?.response || !Array.isArray(response.response)) return []; - return response.response.map(p => ({ + return response.response.map((p) => ({ ...p, id: `${p.playerId}-${category}`, category, lastUpdated: new Date(), })); -} +}; -const get = async ({category = 'overall', page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => { - const response = await queue.ACCSABER.ranking(category, page, priority, queueOptions); +const get = async ({ + category = "overall", + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => { + const response = await queue.ACCSABER.ranking( + category, + page, + priority, + queueOptions, + ); - return {...response, body: {response: response.body, fetchOptions: {category}}} -} + return { + ...response, + body: { response: response.body, fetchOptions: { category } }, + }; +}; const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/accsaber/api-scores.js b/src/network/clients/accsaber/api-scores.js index 6fc55e0..d4982b0 100644 --- a/src/network/clients/accsaber/api-scores.js +++ b/src/network/clients/accsaber/api-scores.js @@ -1,12 +1,13 @@ -import queue from '../../queues/queues' -import createClient from '../generic' -import {dateFromString} from '../../../utils/date' +import queue from "../../queues/queues"; +import createClient from "../generic"; +import { dateFromString } from "../../../utils/date"; -const process = response => { +const process = (response) => { const playerId = response?.fetchOptions?.playerId ?? null; - if (!response?.response || !Array.isArray(response.response) || !playerId) return []; + if (!response?.response || !Array.isArray(response.response) || !playerId) + return []; - return response.response.map(s => { + return response.response.map((s) => { let { songHash: hash, songName: name, @@ -28,11 +29,27 @@ const process = response => { leaderboardId = parseInt(leaderboardId, 10); if (isNaN(leaderboardId)) leaderboardId = null; - const song = {hash, name, subName: '', authorName, levelAuthorName, beatsaverKey}; - const diffInfo = {type: 'Standard', diff: difficulty?.toLowerCase()?.replace('plus', 'Plus')} - const leaderboard = {leaderboardId, song, diffInfo, complexity, categoryDisplayName}; + const song = { + hash, + name, + subName: "", + authorName, + levelAuthorName, + beatsaverKey, + }; + const diffInfo = { + type: "Standard", + diff: difficulty?.toLowerCase()?.replace("plus", "Plus"), + }; + const leaderboard = { + leaderboardId, + song, + diffInfo, + complexity, + categoryDisplayName, + }; - const timeSet = dateFromString(s.timeSet) + const timeSet = dateFromString(s.timeSet); return { id: `${playerId}-${s.leaderboardId}`, playerId, @@ -41,19 +58,42 @@ const process = response => { ap, acc, leaderboard, - score: {...originalScore, ap, unmodifiedScore: score, score, mods: null, timeSet, acc, percentage: acc, weightedAp}, + score: { + ...originalScore, + ap, + unmodifiedScore: score, + score, + mods: null, + timeSet, + acc, + percentage: acc, + weightedAp, + }, fetchedAt: new Date(), lastUpdated: new Date(), - } + }; }); -} +}; -const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => { - const response = await queue.ACCSABER.scores(playerId, page, priority, queueOptions); +const get = async ({ + playerId, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => { + const response = await queue.ACCSABER.scores( + playerId, + page, + priority, + queueOptions, + ); - return {...response, body: {response: response.body, fetchOptions: {playerId, page}}} -} + return { + ...response, + body: { response: response.body, fetchOptions: { playerId, page } }, + }; +}; const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/beatmaps/api-hash.js b/src/network/clients/beatmaps/api-hash.js index e83b831..770ab79 100644 --- a/src/network/clients/beatmaps/api-hash.js +++ b/src/network/clients/beatmaps/api-hash.js @@ -1,9 +1,13 @@ -import queue from '../../queues/queues' -import createClient from '../generic' -import process from './utils/process' +import queue from "../../queues/queues"; +import createClient from "../generic"; +import process from "./utils/process"; -const get = async ({hash, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATMAPS.byHash(hash, priority, queueOptions); +const get = async ({ + hash, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.BEATMAPS.byHash(hash, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/beatmaps/api-key.js b/src/network/clients/beatmaps/api-key.js index fca4e52..02de149 100644 --- a/src/network/clients/beatmaps/api-key.js +++ b/src/network/clients/beatmaps/api-key.js @@ -1,9 +1,13 @@ -import queue from '../../queues/queues' -import createClient from '../generic' -import process from './utils/process' +import queue from "../../queues/queues"; +import createClient from "../generic"; +import process from "./utils/process"; -const get = async ({key, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATMAPS.byKey(key, priority, queueOptions); +const get = async ({ + key, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.BEATMAPS.byKey(key, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/beatmaps/utils/process.js b/src/network/clients/beatmaps/utils/process.js index e514457..5147a09 100644 --- a/src/network/clients/beatmaps/utils/process.js +++ b/src/network/clients/beatmaps/utils/process.js @@ -1,15 +1,15 @@ -import {opt} from '../../../../utils/js' +import { opt } from "../../../../utils/js"; -export default response => { - const versions = opt(response, 'versions'); +export default (response) => { + const versions = opt(response, "versions"); if (!versions || !Array.isArray(versions) || !versions.length) return null; const lastIdx = versions.length - 1; const hash = opt(versions, `${lastIdx}.hash`); - const key = opt(response, 'id'); + const key = opt(response, "id"); if (!hash || !key || !hash.toLowerCase) return null; - return {...response, hash: hash.toLowerCase(), key} -} \ No newline at end of file + return { ...response, hash: hash.toLowerCase(), key }; +}; diff --git a/src/network/clients/beatsavior/api.js b/src/network/clients/beatsavior/api.js index 07928f7..b20b4d8 100644 --- a/src/network/clients/beatsavior/api.js +++ b/src/network/clients/beatsavior/api.js @@ -1,6 +1,6 @@ -import queue from '../../queues/queues' -import {dateFromString} from '../../../utils/date' -import createClient from '../generic' +import queue from "../../queues/queues"; +import { dateFromString } from "../../../utils/date"; +import createClient from "../generic"; const SONG_DATA_TYPES = { None: 0, @@ -8,14 +8,14 @@ const SONG_DATA_TYPES = { Fail: 2, Practice: 3, Replay: 4, - Campaign: 5 -} + Campaign: 5, +}; -const process = response => { +const process = (response) => { if (!response || !Array.isArray(response)) return null; return response - .map(s => { + .map((s) => { let { _id: beatSaviorId, playerID: playerId, @@ -29,28 +29,65 @@ const process = response => { timeSet, trackers, trackers: { - accuracyTracker: {accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence, leftPreswing, leftPostswing, rightPreswing, rightPostswing}, - winTracker: {won, nbOfPause: pauses, rank}, - hitTracker: {bombHit, miss, missedNotes, badCuts, nbOfWallHit: wallHit, maxCombo}, - scoreTracker: {score}, + accuracyTracker: { + accLeft, + accRight, + leftAverageCut, + rightAverageCut, + leftTimeDependence, + rightTimeDependence, + leftPreswing, + leftPostswing, + rightPreswing, + rightPostswing, + }, + winTracker: { won, nbOfPause: pauses, rank }, + hitTracker: { + bombHit, + miss, + missedNotes, + badCuts, + nbOfWallHit: wallHit, + maxCombo, + }, + scoreTracker: { score }, }, } = s; - if (![SONG_DATA_TYPES.Pass, SONG_DATA_TYPES.Fail, SONG_DATA_TYPES.Campaign].includes(type)) return null; + if ( + ![ + SONG_DATA_TYPES.Pass, + SONG_DATA_TYPES.Fail, + SONG_DATA_TYPES.Campaign, + ].includes(type) + ) + return null; const leaderboardId = null; hash = hash ? hash.toLowerCase() : null; - if (!playerId || !playerId.length || !hash || !hash.length || !diff || !diff.length || !score) return null; + if ( + !playerId || + !playerId.length || + !hash || + !hash.length || + !diff || + !diff.length || + !score + ) + return null; - const song = {hash, name, subName: '', authorName, levelAuthorName}; + const song = { hash, name, subName: "", authorName, levelAuthorName }; const leaderboard = { leaderboardId, difficulty, - diffInfo: {diff: diff === 'expertplus' ? 'expertPlus' : diff, type: 'Standard'}, + diffInfo: { + diff: diff === "expertplus" ? "expertPlus" : diff, + type: "Standard", + }, song, - } + }; const stats = { won, @@ -62,9 +99,17 @@ const process = response => { bombHit, wallHit, maxCombo, - accLeft, accRight, leftAverageCut, rightAverageCut, leftTimeDependence, rightTimeDependence, - leftPreswing, leftPostswing, rightPreswing, rightPostswing, - } + accLeft, + accRight, + leftAverageCut, + rightAverageCut, + leftTimeDependence, + rightTimeDependence, + leftPreswing, + leftPostswing, + rightPreswing, + rightPostswing, + }; return { beatSaviorId, @@ -72,24 +117,27 @@ const process = response => { leaderboardId, scoreId: null, hash, - diff: diff === 'expertplus' ? 'expertPlus' : diff, + diff: diff === "expertplus" ? "expertPlus" : diff, score, type, leaderboard, timeSet: dateFromString(timeSet), stats, trackers, - } - + }; }) - .filter(s => s); + .filter((s) => s); }; -const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.BEATSAVIOR.player(playerId, priority, queueOptions); +const get = async ({ + playerId, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.BEATSAVIOR.player(playerId, priority, queueOptions); const client = createClient(get, process); export default { ...client, - SONG_DATA_TYPES -}; \ No newline at end of file + SONG_DATA_TYPES, +}; diff --git a/src/network/clients/generic.js b/src/network/clients/generic.js index f6f7d97..2a99025 100644 --- a/src/network/clients/generic.js +++ b/src/network/clients/generic.js @@ -1,19 +1,35 @@ -import queue, {getResponseBody, isResponseCached, updateResponseBody} from '../queues/queues' +import queue, { + getResponseBody, + isResponseCached, + updateResponseBody, +} from "../queues/queues"; export default (get, process) => { - const clientGet = async ({priority = queue.PRIORITY.FG_LOW, fullResponse = false, ...getOptions} = {}) => { - const response = await get({...getOptions, priority}); + const clientGet = async ({ + priority = queue.PRIORITY.FG_LOW, + fullResponse = false, + ...getOptions + } = {}) => { + const response = await get({ ...getOptions, priority }); return fullResponse ? response : getResponseBody(response); - } + }; - const clientGetProcessed = async ({priority = queue.PRIORITY.FG_LOW, fullResponse = false, ...getOptions} = {}) => { - const response = await clientGet({...getOptions, priority, fullResponse}); + const clientGetProcessed = async ({ + priority = queue.PRIORITY.FG_LOW, + fullResponse = false, + ...getOptions + } = {}) => { + const response = await clientGet({ ...getOptions, priority, fullResponse }); - const processedResponse = process(fullResponse ? getResponseBody(response) : response); + const processedResponse = process( + fullResponse ? getResponseBody(response) : response, + ); - return fullResponse ? updateResponseBody(response, processedResponse) : processedResponse; - } + return fullResponse + ? updateResponseBody(response, processedResponse) + : processedResponse; + }; return { get: clientGet, @@ -21,5 +37,5 @@ export default (get, process) => { getProcessed: clientGetProcessed, getDataFromResponse: getResponseBody, isResponseCached, - } -} \ No newline at end of file + }; +}; diff --git a/src/network/clients/scoresaber/leaderboard/page-leaderboard.js b/src/network/clients/scoresaber/leaderboard/page-leaderboard.js index 2b5a520..a9afbb6 100644 --- a/src/network/clients/scoresaber/leaderboard/page-leaderboard.js +++ b/src/network/clients/scoresaber/leaderboard/page-leaderboard.js @@ -1,34 +1,63 @@ -import queue from '../../../queues/queues' -import {opt} from '../../../../utils/js' -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import { opt } from "../../../../utils/js"; +import createClient from "../../generic"; -const process = response => { - if (!opt(response, 'scores') || !Array.isArray(response.scores)) return null; +const process = (response) => { + if (!opt(response, "scores") || !Array.isArray(response.scores)) return null; - const scores = response.scores.map(s => { - let {unmodififiedScore: unmodifiedScore, mods, ...score} = s.score; + const scores = response.scores.map((s) => { + let { unmodififiedScore: unmodifiedScore, mods, ...score } = s.score; - if (mods && typeof mods === 'string') mods = mods.split(',').map(m => m.trim().toUpperCase()).filter(m => m.length); + if (mods && typeof mods === "string") + mods = mods + .split(",") + .map((m) => m.trim().toUpperCase()) + .filter((m) => m.length); else if (!mods) mods = null; - const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : opt(score, 'acc', null); - const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : opt(score, 'percentage', null); + const acc = + unmodifiedScore && opt(score, "maxScore") + ? (unmodifiedScore / score.maxScore) * 100 + : opt(score, "acc", null); + const percentage = + opt(score, "score") && opt(score, "maxScore") + ? (score.score / score.maxScore) * 100 + : opt(score, "percentage", null); - const ppWeighted = opt(score, 'pp') && opt(score, 'weight') ? score.pp * score.weight : null; + const ppWeighted = + opt(score, "pp") && opt(score, "weight") ? score.pp * score.weight : null; return { ...s, - score: {...score, unmodifiedScore: unmodifiedScore || null, mods, acc, percentage, ppWeighted}, + score: { + ...score, + unmodifiedScore: unmodifiedScore || null, + mods, + acc, + percentage, + ppWeighted, + }, }; }); return { ...response, - scores - } -} + scores, + }; +}; -const get = async ({leaderboardId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.leaderboard(leaderboardId, page, priority, queueOptions); +const get = async ({ + leaderboardId, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => + queue.SCORESABER_PAGE.leaderboard( + leaderboardId, + page, + priority, + queueOptions, + ); const client = createClient(get, process); diff --git a/src/network/clients/scoresaber/player/api.js b/src/network/clients/scoresaber/player/api.js index 40ddfed..82ba530 100644 --- a/src/network/clients/scoresaber/player/api.js +++ b/src/network/clients/scoresaber/player/api.js @@ -1,34 +1,56 @@ -import queue from '../../../queues/queues' -import createClient from '../../generic' -import {opt} from '../../../../utils/js' +import queue from "../../../queues/queues"; +import createClient from "../../generic"; +import { opt } from "../../../../utils/js"; -const process = response => { - if (!opt(response, 'playerInfo')) return null; +const process = (response) => { + if (!opt(response, "playerInfo")) return null; - const {playerInfo: info, scoreStats} = response; - const {playerId, playerName: name, country, countryRank, avatar, permissions, ...playerInfo} = info; + const { playerInfo: info, scoreStats } = response; + const { + playerId, + playerName: name, + country, + countryRank, + avatar, + permissions, + ...playerInfo + } = info; if (avatar) { - if (!avatar.startsWith('http')) - playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`; - else - playerInfo.avatar = avatar; + if (!avatar.startsWith("http")) + playerInfo.avatar = `${queue.SCORESABER_API.SS_API_HOST}${ + !avatar.startsWith("/") ? "/" : "" + }${avatar}`; + else playerInfo.avatar = avatar; } playerInfo.banned = !!playerInfo.banned; playerInfo.inactive = !!playerInfo.inactive; - playerInfo.rankHistory = playerInfo.history && playerInfo.history.length - ? playerInfo.history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r)) - : []; + playerInfo.rankHistory = + playerInfo.history && playerInfo.history.length + ? playerInfo.history + .split(",") + .map((r) => parseInt(r, 10)) + .filter((r) => !isNaN(r)) + : []; delete playerInfo.history; playerInfo.externalProfileUrl = null; - playerInfo.countries = [{country, rank: countryRank}]; + playerInfo.countries = [{ country, rank: countryRank }]; - return {playerId, name, playerInfo, scoreStats: scoreStats ? scoreStats : null}; + return { + playerId, + name, + playerInfo, + scoreStats: scoreStats ? scoreStats : null, + }; }; -const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.player(playerId, priority, queueOptions); +const get = async ({ + playerId, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.SCORESABER_API.player(playerId, priority, queueOptions); const client = createClient(get, process); diff --git a/src/network/clients/scoresaber/player/page.js b/src/network/clients/scoresaber/player/page.js index 06b9620..eb6bc89 100644 --- a/src/network/clients/scoresaber/player/page.js +++ b/src/network/clients/scoresaber/player/page.js @@ -1,21 +1,27 @@ -import queue from '../../../queues/queues' -import api from './api' -import {opt} from '../../../../utils/js' -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import api from "./api"; +import { opt } from "../../../../utils/js"; +import createClient from "../../generic"; -const process = response => { - const apiProcessedResponse = api.process(response && response.player ? response.player : null); +const process = (response) => { + const apiProcessedResponse = api.process( + response && response.player ? response.player : null, + ); - if (!opt(apiProcessedResponse, 'player.playerInfo')) return null; + if (!opt(apiProcessedResponse, "player.playerInfo")) return null; - const recentPlay = opt(response, 'player.recentPlay'); - const recentPlayLastUpdated = opt(response, 'player.recentPlayLastUpdated'); + const recentPlay = opt(response, "player.recentPlay"); + const recentPlayLastUpdated = opt(response, "player.recentPlayLastUpdated"); if (recentPlay && recentPlayLastUpdated) { apiProcessedResponse.playerInfo.recentPlay = recentPlay; - apiProcessedResponse.playerInfo.recentPlayLastUpdated = recentPlayLastUpdated; + apiProcessedResponse.playerInfo.recentPlayLastUpdated = + recentPlayLastUpdated; } - const externalProfileUrl = opt(response, 'player.playerInfo.externalProfileUrl'); + const externalProfileUrl = opt( + response, + "player.playerInfo.externalProfileUrl", + ); if (externalProfileUrl) { apiProcessedResponse.playerInfo.externalProfileUrl = externalProfileUrl; } @@ -23,8 +29,12 @@ const process = response => { return apiProcessedResponse; }; -const get = async ({playerId, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.player(playerId, priority, queueOptions); +const get = async ({ + playerId, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.SCORESABER_PAGE.player(playerId, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/players/api-player-find.js b/src/network/clients/scoresaber/players/api-player-find.js index a74c3f4..7512ff5 100644 --- a/src/network/clients/scoresaber/players/api-player-find.js +++ b/src/network/clients/scoresaber/players/api-player-find.js @@ -1,9 +1,13 @@ -import queue from '../../../queues/queues' -import process from './utils/process' -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import process from "./utils/process"; +import createClient from "../../generic"; -const get = async ({query, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.findPlayer(query, priority, queueOptions); +const get = async ({ + query, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.SCORESABER_API.findPlayer(query, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/players/api-ranking-global-pages.js b/src/network/clients/scoresaber/players/api-ranking-global-pages.js index 0c829c2..33e8af4 100644 --- a/src/network/clients/scoresaber/players/api-ranking-global-pages.js +++ b/src/network/clients/scoresaber/players/api-ranking-global-pages.js @@ -1,11 +1,14 @@ -import queue from '../../../queues/queues' -import {opt} from '../../../../utils/js' -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import { opt } from "../../../../utils/js"; +import createClient from "../../generic"; -const process = response => opt(response, 'pages', null) +const process = (response) => opt(response, "pages", null); -const get = async ({priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.rankingGlobalPages(priority, queueOptions); +const get = async ({ + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.SCORESABER_API.rankingGlobalPages(priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/players/api-ranking-global.js b/src/network/clients/scoresaber/players/api-ranking-global.js index f2f083f..1ee1215 100644 --- a/src/network/clients/scoresaber/players/api-ranking-global.js +++ b/src/network/clients/scoresaber/players/api-ranking-global.js @@ -1,9 +1,13 @@ -import queue from '../../../queues/queues' -import process from './utils/process' -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import process from "./utils/process"; +import createClient from "../../generic"; -const get = async ({page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.rankingGlobal(page, priority, queueOptions); +const get = async ({ + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.SCORESABER_API.rankingGlobal(page, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/players/page-ranking-country.js b/src/network/clients/scoresaber/players/page-ranking-country.js index fb1f747..793912c 100644 --- a/src/network/clients/scoresaber/players/page-ranking-country.js +++ b/src/network/clients/scoresaber/players/page-ranking-country.js @@ -1,18 +1,24 @@ -import queue from '../../../queues/queues' -import api from './api-ranking-global' -import {opt} from '../../../../utils/js' -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import api from "./api-ranking-global"; +import { opt } from "../../../../utils/js"; +import createClient from "../../generic"; -const process = response => { +const process = (response) => { const apiProcessedResponse = api.process(response); - if (!opt(response, 'players')) return null; + if (!opt(response, "players")) return null; return apiProcessedResponse; -} +}; -const get = async ({country, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_PAGE.countryRanking(country, page, priority, queueOptions); +const get = async ({ + country, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => + queue.SCORESABER_PAGE.countryRanking(country, page, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/players/utils/process.js b/src/network/clients/scoresaber/players/utils/process.js index 94929ee..c1e046c 100644 --- a/src/network/clients/scoresaber/players/utils/process.js +++ b/src/network/clients/scoresaber/players/utils/process.js @@ -1,17 +1,28 @@ -import {opt} from '../../../../../utils/js' -import queue from '../../../../queues/queues' +import { opt } from "../../../../../utils/js"; +import queue from "../../../../queues/queues"; -export default response => { - if (!opt(response, 'players')) return null; +export default (response) => { + if (!opt(response, "players")) return null; if (!Array.isArray(response.players)) return null; - return response.players.map(player => { - let {avatar, country, difference, history, playerId, playerName: name, pp, rank} = player; + return response.players.map((player) => { + let { + avatar, + country, + difference, + history, + playerId, + playerName: name, + pp, + rank, + } = player; if (avatar) { - if (!avatar.startsWith('http')) - avatar = `${queue.SCORESABER_API.SS_API_HOST}${!avatar.startsWith('/') ? '/' : ''}${avatar}`; + if (!avatar.startsWith("http")) + avatar = `${queue.SCORESABER_API.SS_API_HOST}${ + !avatar.startsWith("/") ? "/" : "" + }${avatar}`; } return { @@ -19,16 +30,20 @@ export default response => { name, playerInfo: { avatar, - countries: [{country, rank: null}], + countries: [{ country, rank: null }], pp, rank, - rankHistory: history && history.length - ? history.split(',').map(r => parseInt(r, 10)).filter(r => !isNaN(r)) - : [], + rankHistory: + history && history.length + ? history + .split(",") + .map((r) => parseInt(r, 10)) + .filter((r) => !isNaN(r)) + : [], }, others: { difference, }, - } + }; }); -}; \ No newline at end of file +}; diff --git a/src/network/clients/scoresaber/rankeds/page.js b/src/network/clients/scoresaber/rankeds/page.js index 11de639..5a68df2 100644 --- a/src/network/clients/scoresaber/rankeds/page.js +++ b/src/network/clients/scoresaber/rankeds/page.js @@ -1,10 +1,14 @@ -import createClient from '../../generic' -import queues from '../../../queues/queues' +import createClient from "../../generic"; +import queues from "../../../queues/queues"; -const process = response => response; +const process = (response) => response; -const get = async ({page = 1, priority = queues.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queues.SCORESABER_PAGE.rankeds(page, priority, queueOptions) +const get = async ({ + page = 1, + priority = queues.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queues.SCORESABER_PAGE.rankeds(page, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/scores/api-recent.js b/src/network/clients/scoresaber/scores/api-recent.js index dacffad..8673d2c 100644 --- a/src/network/clients/scoresaber/scores/api-recent.js +++ b/src/network/clients/scoresaber/scores/api-recent.js @@ -1,9 +1,15 @@ -import queue from '../../../queues/queues' -import process from './utils/process'; -import createClient from '../../generic' +import queue from "../../../queues/queues"; +import process from "./utils/process"; +import createClient from "../../generic"; -const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.recentScores(playerId, page, priority, queueOptions); +const get = async ({ + playerId, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => + queue.SCORESABER_API.recentScores(playerId, page, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/scores/api-top.js b/src/network/clients/scoresaber/scores/api-top.js index a93ac20..f8fd21b 100644 --- a/src/network/clients/scoresaber/scores/api-top.js +++ b/src/network/clients/scoresaber/scores/api-top.js @@ -1,9 +1,15 @@ -import queue from '../../../queues/queues' -import createClient from '../../generic' -import process from './utils/process' +import queue from "../../../queues/queues"; +import createClient from "../../generic"; +import process from "./utils/process"; -const get = async ({playerId, page = 1, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.SCORESABER_API.topScores(playerId, page, priority, queueOptions); +const get = async ({ + playerId, + page = 1, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => + queue.SCORESABER_API.topScores(playerId, page, priority, queueOptions); const client = createClient(get, process); -export default client; \ No newline at end of file +export default client; diff --git a/src/network/clients/scoresaber/scores/utils/process.js b/src/network/clients/scoresaber/scores/utils/process.js index d2dc63d..ac8ff86 100644 --- a/src/network/clients/scoresaber/scores/utils/process.js +++ b/src/network/clients/scoresaber/scores/utils/process.js @@ -1,11 +1,16 @@ -import {dateFromString} from '../../../../../utils/date' -import {extractDiffAndType} from '../../../../../utils/scoresaber/format' -import {opt} from '../../../../../utils/js' +import { dateFromString } from "../../../../../utils/date"; +import { extractDiffAndType } from "../../../../../utils/scoresaber/format"; +import { opt } from "../../../../../utils/js"; -export default response => { - if (!opt(response, 'scores') || !Array.isArray(response.scores) || !opt(response, 'scores.0.scoreId')) return []; +export default (response) => { + if ( + !opt(response, "scores") || + !Array.isArray(response.scores) || + !opt(response, "scores.0.scoreId") + ) + return []; - return response.scores.map(s => { + return response.scores.map((s) => { const { songHash: hash, songName: name, @@ -18,25 +23,44 @@ export default response => { ...originalScore } = s; - const song = {hash, name, subName, authorName, levelAuthorName}; + const song = { hash, name, subName, authorName, levelAuthorName }; const diffInfo = extractDiffAndType(difficultyRaw); - const leaderboard = {leaderboardId, song, diffInfo, difficulty}; + const leaderboard = { leaderboardId, song, diffInfo, difficulty }; - let {unmodififiedScore: unmodifiedScore, mods, ...score} = originalScore; + let { unmodififiedScore: unmodifiedScore, mods, ...score } = originalScore; - if (mods && typeof mods === 'string') mods = mods.split(',').map(m => m.trim().toUpperCase()).filter(m => m.length); + if (mods && typeof mods === "string") + mods = mods + .split(",") + .map((m) => m.trim().toUpperCase()) + .filter((m) => m.length); else if (!mods) mods = null; - const acc = unmodifiedScore && opt(score, 'maxScore') ? unmodifiedScore / score.maxScore * 100 : null; - const percentage = opt(score, 'score') && opt(score, 'maxScore') ? score.score / score.maxScore * 100 : null; + const acc = + unmodifiedScore && opt(score, "maxScore") + ? (unmodifiedScore / score.maxScore) * 100 + : null; + const percentage = + opt(score, "score") && opt(score, "maxScore") + ? (score.score / score.maxScore) * 100 + : null; - const ppWeighted = opt(score, 'pp') && opt(score, 'weight') ? score.pp * score.weight : null; + const ppWeighted = + opt(score, "pp") && opt(score, "weight") ? score.pp * score.weight : null; return { leaderboard, - score: {...score, unmodifiedScore, mods, timeSet: dateFromString(score.timeSet), acc, percentage, ppWeighted}, + score: { + ...score, + unmodifiedScore, + mods, + timeSet: dateFromString(score.timeSet), + acc, + percentage, + ppWeighted, + }, fetchedAt: new Date(), lastUpdated: new Date(), }; }); -} \ No newline at end of file +}; diff --git a/src/network/clients/twitch/api-profile.js b/src/network/clients/twitch/api-profile.js index 4ebe15f..70e95af 100644 --- a/src/network/clients/twitch/api-profile.js +++ b/src/network/clients/twitch/api-profile.js @@ -1,17 +1,22 @@ -import queue from '../../queues/queues' -import createClient from '../generic' -import {opt} from '../../../utils/js' +import queue from "../../queues/queues"; +import createClient from "../generic"; +import { opt } from "../../../utils/js"; -const process = response => { - if (!opt(response, 'data.0')) return null; +const process = (response) => { + if (!opt(response, "data.0")) return null; - return {...response.data[0], profileLastUpdated: new Date()}; + return { ...response.data[0], profileLastUpdated: new Date() }; }; -const get = async ({accessToken, login, priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.TWITCH.profile(accessToken, login, priority, queueOptions); +const get = async ({ + accessToken, + login, + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.TWITCH.profile(accessToken, login, priority, queueOptions); const client = createClient(get, process); export default { - ...client -} + ...client, +}; diff --git a/src/network/clients/twitch/api-videos.js b/src/network/clients/twitch/api-videos.js index a818ffd..3c4e074 100644 --- a/src/network/clients/twitch/api-videos.js +++ b/src/network/clients/twitch/api-videos.js @@ -1,16 +1,22 @@ -import queue from '../../queues/queues' -import createClient from '../generic' +import queue from "../../queues/queues"; +import createClient from "../generic"; -const process = response => { +const process = (response) => { if (!response || !response.data || !Array.isArray(response.data)) return null; return response.data; }; -const get = async ({accessToken, userId, type = 'archive', priority = queue.PRIORITY.FG_HIGH, ...queueOptions} = {}) => queue.TWITCH.videos(accessToken, userId, type, queueOptions); +const get = async ({ + accessToken, + userId, + type = "archive", + priority = queue.PRIORITY.FG_HIGH, + ...queueOptions +} = {}) => queue.TWITCH.videos(accessToken, userId, type, queueOptions); const client = createClient(get, process); export default { ...client, -} +}; diff --git a/src/network/command-processor.js b/src/network/command-processor.js index 1900eca..bb3fe4d 100644 --- a/src/network/command-processor.js +++ b/src/network/command-processor.js @@ -1,45 +1,48 @@ -import eventBus from '../utils/broadcast-channel-pubsub' -import createPlayerService from '../services/scoresaber/player' -import log from '../utils/logger' +import eventBus from "../utils/broadcast-channel-pubsub"; +import createPlayerService from "../services/scoresaber/player"; +import log from "../utils/logger"; let initialized = false; export default (dlManager) => { if (initialized) { - log.debug(`Command processor already initialized.`, 'CmdProcessor'); + log.debug(`Command processor already initialized.`, "CmdProcessor"); return; } const playerService = createPlayerService(); - eventBus.on('data-imported', () => { - if (window) window.location.reload() + eventBus.on("data-imported", () => { + if (window) window.location.reload(); }); - eventBus.on('player-add-cmd', async ({playerId}) => { + eventBus.on("player-add-cmd", async ({ playerId }) => { await dlManager.enqueuePlayer(playerId); }); - eventBus.on('player-remove-cmd', async ({playerId, purgeScores = false}) => { - if (!playerId) return; + eventBus.on( + "player-remove-cmd", + async ({ playerId, purgeScores = false }) => { + if (!playerId) return; - await playerService.remove(playerId, purgeScores); - }); + await playerService.remove(playerId, purgeScores); + }, + ); - eventBus.on('dl-manager-pause-cmd', () => { - log.debug('Pause Dl Manager', 'CmdProcessor'); + eventBus.on("dl-manager-pause-cmd", () => { + log.debug("Pause Dl Manager", "CmdProcessor"); dlManager.pause(); }); - eventBus.on('dl-manager-unpause-cmd', () => { - log.debug('Unpause Dl Manager', 'CmdProcessor'); + eventBus.on("dl-manager-unpause-cmd", () => { + log.debug("Unpause Dl Manager", "CmdProcessor"); dlManager.start(); }); initialized = true; - log.info(`Command processor initialized`, 'CmdProcessor'); -} \ No newline at end of file + log.info(`Command processor initialized`, "CmdProcessor"); +}; diff --git a/src/network/download-manager.js b/src/network/download-manager.js index 4a37638..3ce8c26 100644 --- a/src/network/download-manager.js +++ b/src/network/download-manager.js @@ -1,15 +1,15 @@ -import eventBus from '../utils/broadcast-channel-pubsub' -import log from '../utils/logger' -import createQueue, {PRIORITY} from '../utils/queue' -import {configStore} from '../stores/config' -import createRankedsStore from '../stores/scoresaber/rankeds' -import createPlayerService from '../services/scoresaber/player' -import createScoresService from '../services/scoresaber/scores' -import createBeatSaviorService from '../services/beatsavior' -import createAccSaberService from '../services/accsaber' -import {PRIORITY as HTTP_QUEUE_PRIORITY} from './queues/http-queue' -import {HOUR, MINUTE} from '../utils/date' -import {opt} from '../utils/js' +import eventBus from "../utils/broadcast-channel-pubsub"; +import log from "../utils/logger"; +import createQueue, { PRIORITY } from "../utils/queue"; +import { configStore } from "../stores/config"; +import createRankedsStore from "../stores/scoresaber/rankeds"; +import createPlayerService from "../services/scoresaber/player"; +import createScoresService from "../services/scoresaber/scores"; +import createBeatSaviorService from "../services/beatsavior"; +import createAccSaberService from "../services/accsaber"; +import { PRIORITY as HTTP_QUEUE_PRIORITY } from "./queues/http-queue"; +import { HOUR, MINUTE } from "../utils/date"; +import { opt } from "../utils/js"; const INTERVAL_TICK = MINUTE; @@ -22,110 +22,198 @@ let beatSaviorService = null; let accSaberService = null; const TYPES = { - BEATSAVIOR: {name: 'BEATSAVIOR', priority: PRIORITY.LOW}, - RANKEDS: {name: 'RANKEDS', priority: PRIORITY.LOW}, - ACCSABER: {name: 'ACCSABER', priority: PRIORITY.NORMAL}, - PLAYER_SCORES: {name: 'PLAYER-SCORES', priority: PRIORITY.NORMAL}, - PLAYER_SCORES_UPDATE_QUEUE: {name: 'PLAYER_SCORES_UPDATE_QUEUE', priority: PRIORITY.LOWEST}, - ACTIVE_PLAYERS: {name: 'ACTIVE-PLAYERS', priority: PRIORITY.HIGH}, - MAIN_PLAYER: {name: 'MAIN-PLAYER', priority: PRIORITY.HIGHEST}, -} + BEATSAVIOR: { name: "BEATSAVIOR", priority: PRIORITY.LOW }, + RANKEDS: { name: "RANKEDS", priority: PRIORITY.LOW }, + ACCSABER: { name: "ACCSABER", priority: PRIORITY.NORMAL }, + PLAYER_SCORES: { name: "PLAYER-SCORES", priority: PRIORITY.NORMAL }, + PLAYER_SCORES_UPDATE_QUEUE: { + name: "PLAYER_SCORES_UPDATE_QUEUE", + priority: PRIORITY.LOWEST, + }, + ACTIVE_PLAYERS: { name: "ACTIVE-PLAYERS", priority: PRIORITY.HIGH }, + MAIN_PLAYER: { name: "MAIN-PLAYER", priority: PRIORITY.HIGHEST }, +}; -const enqueue = async (queue, type, force = false, data = null, then = null) => { +const enqueue = async ( + queue, + type, + force = false, + data = null, + then = null, +) => { if (!type || !type.name || !Number.isFinite(type.priority)) { - log.warn(`Unknown type enqueued.`, 'DlManager', type); + log.warn(`Unknown type enqueued.`, "DlManager", type); return; } - log.debug(`Try to enqueue type ${type.name}. Forced: ${force}, data: ${JSON.stringify(data)}`, 'DlManager'); + log.debug( + `Try to enqueue type ${type.name}. Forced: ${force}, data: ${JSON.stringify( + data, + )}`, + "DlManager", + ); const priority = force ? PRIORITY.HIGHEST : type.priority; - const networkPriority = priority === PRIORITY.HIGHEST ? HTTP_QUEUE_PRIORITY.BG_HIGH : HTTP_QUEUE_PRIORITY.BG_NORMAL; + const networkPriority = + priority === PRIORITY.HIGHEST + ? HTTP_QUEUE_PRIORITY.BG_HIGH + : HTTP_QUEUE_PRIORITY.BG_NORMAL; const processThen = async (promise, then = null) => { - promise.then(result => { - if(then) log.debug('Processing then command...', 'DlManager'); + promise.then((result) => { + if (then) log.debug("Processing then command...", "DlManager"); - return then ? {result, thenResult: then()} : result; - }) - } + return then ? { result, thenResult: then() } : result; + }); + }; switch (type) { case TYPES.MAIN_PLAYER: if (mainPlayerId) { - log.debug(`Enqueue main player`, 'DlManager'); + log.debug(`Enqueue main player`, "DlManager"); await Promise.all([ - enqueue(queue, {...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}), - enqueue(queue, {...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST}, force, {playerId: mainPlayerId}), + enqueue( + queue, + { ...TYPES.ACTIVE_PLAYERS, priority: PRIORITY.HIGHEST }, + force, + { playerId: mainPlayerId }, + ), + enqueue( + queue, + { ...TYPES.PLAYER_SCORES, priority: PRIORITY.HIGHEST }, + force, + { playerId: mainPlayerId }, + ), ]); } break; case TYPES.RANKEDS: - log.debug(`Enqueue rankeds`, 'DlManager'); + log.debug(`Enqueue rankeds`, "DlManager"); if (!rankedsStore) rankedsStore = await createRankedsStore(); - processThen(queue.add(async () => rankedsStore.refresh(force, networkPriority), priority), then) - .then(_ => log.debug('Enqueued rankeds processed.', 'DlManager')); + processThen( + queue.add( + async () => rankedsStore.refresh(force, networkPriority), + priority, + ), + then, + ).then((_) => log.debug("Enqueued rankeds processed.", "DlManager")); break; case TYPES.ACTIVE_PLAYERS: - log.debug(`Enqueue active players`, 'DlManager'); + log.debug(`Enqueue active players`, "DlManager"); if (data && data.playerId) { if (data.add) - processThen(queue.add(async () => playerService.add(data.playerId, networkPriority), priority), then) - .then(_ => log.debug('Enqueued active players processed.', 'DlManager')); + processThen( + queue.add( + async () => playerService.add(data.playerId, networkPriority), + priority, + ), + then, + ).then((_) => + log.debug("Enqueued active players processed.", "DlManager"), + ); else - processThen(queue.add(async () => playerService.refresh(data.playerId, force, networkPriority), priority), then) - .then(_ => log.debug('Enqueued active players processed.', 'DlManager')); + processThen( + queue.add( + async () => + playerService.refresh(data.playerId, force, networkPriority), + priority, + ), + then, + ).then((_) => + log.debug("Enqueued active players processed.", "DlManager"), + ); } else - processThen(queue.add(async () => playerService.refreshAll(force, networkPriority), priority), then) - .then(_ => log.debug('Enqueued active players processed.', 'DlManager')); + processThen( + queue.add( + async () => playerService.refreshAll(force, networkPriority), + priority, + ), + then, + ).then((_) => + log.debug("Enqueued active players processed.", "DlManager"), + ); break; case TYPES.PLAYER_SCORES: - log.debug(`Enqueue players scores`, 'DlManager'); + log.debug(`Enqueue players scores`, "DlManager"); if (data && data.playerId) - processThen(queue.add(async () => scoresService.refresh(data.playerId, force, networkPriority), priority), then) - .then(_ => log.debug('Enqueued players scores processed.', 'DlManager')); + processThen( + queue.add( + async () => + scoresService.refresh(data.playerId, force, networkPriority), + priority, + ), + then, + ).then((_) => + log.debug("Enqueued players scores processed.", "DlManager"), + ); else - processThen(queue.add(async () => scoresService.refreshAll(force, networkPriority), priority), then) - .then(_ => log.debug('Enqueued players scores processed.', 'DlManager')); + processThen( + queue.add( + async () => scoresService.refreshAll(force, networkPriority), + priority, + ), + then, + ).then((_) => + log.debug("Enqueued players scores processed.", "DlManager"), + ); break; case TYPES.BEATSAVIOR: - log.debug(`Enqueue Beat Savior`, 'DlManager'); + log.debug(`Enqueue Beat Savior`, "DlManager"); - processThen(queue.add(async () => beatSaviorService.refreshAll(force, networkPriority), priority), then) - .then(_ => log.debug('Enqueued Beat Savior processed.', 'DlManager')); + processThen( + queue.add( + async () => beatSaviorService.refreshAll(force, networkPriority), + priority, + ), + then, + ).then((_) => log.debug("Enqueued Beat Savior processed.", "DlManager")); break; case TYPES.PLAYER_SCORES_UPDATE_QUEUE: - log.debug(`Enqueue player scores rank && pp updates`, 'DlManager'); + log.debug(`Enqueue player scores rank && pp updates`, "DlManager"); - processThen(queue.add(async () => scoresService.updateRankAndPpFromTheQueue(), priority), then) - .then(_ => log.debug('Enqueued player scores rank & pp updates processed.', 'DlManager')); + processThen( + queue.add( + async () => scoresService.updateRankAndPpFromTheQueue(), + priority, + ), + then, + ).then((_) => + log.debug( + "Enqueued player scores rank & pp updates processed.", + "DlManager", + ), + ); break; case TYPES.ACCSABER: - log.debug(`Enqueue AccSaber updates`, 'DlManager'); + log.debug(`Enqueue AccSaber updates`, "DlManager"); - processThen(queue.add(async () => accSaberService.refreshAll(), priority), then) - .then(_ => log.debug('Enqueued AccSaber updates processed.', 'DlManager')); + processThen( + queue.add(async () => accSaberService.refreshAll(), priority), + then, + ).then((_) => + log.debug("Enqueued AccSaber updates processed.", "DlManager"), + ); break; } -} +}; -const enqueueAllJobs = async queue => { - log.debug(`Try to enqueue & process queue.`, 'DlManager'); +const enqueueAllJobs = async (queue) => { + log.debug(`Try to enqueue & process queue.`, "DlManager"); await Promise.all([ enqueue(queue, TYPES.MAIN_PLAYER), @@ -137,18 +225,18 @@ const enqueueAllJobs = async queue => { // it should be at the end of the queue enqueue(queue, TYPES.PLAYER_SCORES_UPDATE_QUEUE), - ]) -} + ]); +}; let intervalId; -const startSyncing = async queue => { +const startSyncing = async (queue) => { await enqueueAllJobs(queue); intervalId = setInterval(() => enqueueAllJobs(queue), INTERVAL_TICK); -} +}; export default async () => { if (initialized) { - log.debug(`Download manager already initialized.`, 'DlManager'); + log.debug(`Download manager already initialized.`, "DlManager"); return; } @@ -161,49 +249,54 @@ export default async () => { mainPlayerId = configStore.getMainPlayerId(); - configStore.subscribe(config => { - const newMainPlayerId = opt(config, 'users.main') + configStore.subscribe((config) => { + const newMainPlayerId = opt(config, "users.main"); if (mainPlayerId !== newMainPlayerId) { mainPlayerId = newMainPlayerId; - log.debug(`Main player changed to ${mainPlayerId}`, 'DlManager') + log.debug(`Main player changed to ${mainPlayerId}`, "DlManager"); } - }) + }); playerService = createPlayerService(); scoresService = createScoresService(); beatSaviorService = createBeatSaviorService(); accSaberService = createAccSaberService(); - eventBus.leaderStore.subscribe(async isLeader => { + eventBus.leaderStore.subscribe(async (isLeader) => { if (isLeader) { queue.clear(); queue.start(); const nodeId = eventBus.getNodeId(); - log.info(`Node ${nodeId} is a leader, queue processing enabled`, 'DlManager') + log.info( + `Node ${nodeId} is a leader, queue processing enabled`, + "DlManager", + ); - await startSyncing(queue) + await startSyncing(queue); } - }) + }); - const enqueuePlayer = async playerId => { + const enqueuePlayer = async (playerId) => { await enqueue( - queue, TYPES.ACTIVE_PLAYERS, true, - {playerId, add: true}, - async () => enqueue(queue, TYPES.PLAYER_SCORES, true, {playerId}), + queue, + TYPES.ACTIVE_PLAYERS, + true, + { playerId, add: true }, + async () => enqueue(queue, TYPES.PLAYER_SCORES, true, { playerId }), ); - } + }; const pause = () => { - log.debug('Pause Dl Manager', 'DlManager'); + log.debug("Pause Dl Manager", "DlManager"); queue.clear(); queue.pause(); }; const start = () => { - log.debug('Unpause Dl Manager', 'DlManager'); + log.debug("Unpause Dl Manager", "DlManager"); queue.clear(); queue.start(); @@ -213,11 +306,11 @@ export default async () => { initialized = true; - log.info(`Download manager initialized`, 'DlManager'); + log.info(`Download manager initialized`, "DlManager"); return { start, pause, - enqueuePlayer - } -} \ No newline at end of file + enqueuePlayer, + }; +}; diff --git a/src/network/errors.js b/src/network/errors.js index 645aa58..66aea9b 100644 --- a/src/network/errors.js +++ b/src/network/errors.js @@ -1,6 +1,6 @@ -import {SsrError} from '../others/errors' -import {delay} from '../utils/promise' -import {parseRateLimitHeaders} from './utils' +import { SsrError } from "../others/errors"; +import { delay } from "../utils/promise"; +import { parseRateLimitHeaders } from "./utils"; export class SsrNetworkError extends SsrError { constructor(message) { @@ -20,7 +20,7 @@ export class SsrNetworkError extends SsrError { export class SsrNetworkTimeoutError extends SsrNetworkError { constructor(timeout, message) { - super(message && message.length ? message : `Timeout Error (${timeout}ms)`) + super(message && message.length ? message : `Timeout Error (${timeout}ms)`); this.name = "SsrNetworkTimeoutError"; this.timeout = timeout; @@ -29,12 +29,17 @@ export class SsrNetworkTimeoutError extends SsrNetworkError { export class SsrHttpResponseError extends SsrNetworkError { constructor(response, ...args) { - super(`HTTP Error Response: ${response && response.status ? response.status : 'None'} ${response && response.statusText ? response.statusText : ''}`, ...args); + super( + `HTTP Error Response: ${ + response && response.status ? response.status : "None" + } ${response && response.statusText ? response.statusText : ""}`, + ...args, + ); - this.name = 'SsrHttpResponseError'; + this.name = "SsrHttpResponseError"; this.response = response; - const {remaining, limit, resetAt} = parseRateLimitHeaders(response); + const { remaining, limit, resetAt } = parseRateLimitHeaders(response); this.remaining = remaining; this.limit = limit; @@ -50,7 +55,7 @@ export class SsrHttpClientError extends SsrHttpResponseError { constructor(...args) { super(...args); - this.name = 'SsrHttpClientError'; + this.name = "SsrHttpClientError"; } shouldRetry() { @@ -66,7 +71,7 @@ export class SsrHttpRateLimitError extends SsrHttpClientError { constructor(response, ...args) { super(response, ...args); - this.name = 'SsrHttpRateLimitError'; + this.name = "SsrHttpRateLimitError"; } shouldRetry() { @@ -119,6 +124,6 @@ export class SsrHttpServerError extends SsrHttpResponseError { constructor(...args) { super(...args); - this.name = 'SsrHttpServerError'; + this.name = "SsrHttpServerError"; } -} \ No newline at end of file +} diff --git a/src/network/fetch.js b/src/network/fetch.js index e214f3f..a034a28 100644 --- a/src/network/fetch.js +++ b/src/network/fetch.js @@ -97,12 +97,12 @@ export async function fetchUrl(url, options = {}, cors = true) { export async function fetchJson( url, - { cacheTtl = null, maxAge = null, ...restOptions } = {} + { cacheTtl = null, maxAge = null, ...restOptions } = {}, ) { const options = getOptionsWithCacheKey( url, { cacheTtl, maxAge, ...restOptions }, - "json" + "json", ); const { @@ -129,7 +129,7 @@ export async function fetchJson( body, }, fetchCacheKey, - fetchCacheTtl + fetchCacheTtl, ); }) .catch((err) => { @@ -141,12 +141,12 @@ export async function fetchJson( export async function fetchHtml( url, - { cacheTtl = null, maxAge = null, ...restOptions } = {} + { cacheTtl = null, maxAge = null, ...restOptions } = {}, ) { const options = getOptionsWithCacheKey( url, { cacheTtl, maxAge, ...restOptions }, - "json" + "json", ); const { @@ -172,7 +172,7 @@ export async function fetchHtml( body: new DOMParser().parseFromString(body, "text/html"), }, fetchCacheKey, - fetchCacheTtl + fetchCacheTtl, ); }); } diff --git a/src/network/queues/accsaber/api-queue.js b/src/network/queues/accsaber/api-queue.js index cfe0fbe..e7ee0f9 100644 --- a/src/network/queues/accsaber/api-queue.js +++ b/src/network/queues/accsaber/api-queue.js @@ -1,25 +1,75 @@ -import {default as createQueue, PRIORITY} from '../http-queue'; -import {substituteVars} from "../../../utils/format"; +import { default as createQueue, PRIORITY } from "../http-queue"; +import { substituteVars } from "../../../utils/format"; -const ACCSABER_API_URL = 'https://api.accsaber.com'; -const CATEGORIES_URL = ACCSABER_API_URL + '/categories'; -const RANKING_URL = ACCSABER_API_URL + '/categories/${category}/standings'; -const PLAYER_SCORES_URL = ACCSABER_API_URL + '/players/${playerId}/scores'; -const PLAYER_RANK_HISTORY = ACCSABER_API_URL + '/players/${playerId}/recent-rank-history' -const LEADERBOARD_URL = ACCSABER_API_URL + '/map-leaderboards/${leaderboardId}'; -const LEADERBOARD_INFO_URL = ACCSABER_API_URL + '/ranked-maps/${leaderboardId}'; +const ACCSABER_API_URL = "https://api.accsaber.com"; +const CATEGORIES_URL = ACCSABER_API_URL + "/categories"; +const RANKING_URL = ACCSABER_API_URL + "/categories/${category}/standings"; +const PLAYER_SCORES_URL = ACCSABER_API_URL + "/players/${playerId}/scores"; +const PLAYER_RANK_HISTORY = + ACCSABER_API_URL + "/players/${playerId}/recent-rank-history"; +const LEADERBOARD_URL = ACCSABER_API_URL + "/map-leaderboards/${leaderboardId}"; +const LEADERBOARD_INFO_URL = ACCSABER_API_URL + "/ranked-maps/${leaderboardId}"; export default (options = {}) => { const queue = createQueue(options); - const {fetchJson, fetchHtml, ...queueToReturn} = queue; + const { fetchJson, fetchHtml, ...queueToReturn } = queue; - const categories = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(CATEGORIES_URL, options, priority) - const ranking = async (category = 'overall', page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(RANKING_URL, {category, page}), options, priority) - const scores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_SCORES_URL, {playerId, page}), options, priority) - const playerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(PLAYER_RANK_HISTORY, {playerId}), options, priority) - const leaderboard = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_URL, {leaderboardId, page}), options, priority) - const leaderboardInfo = async (leaderboardId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(LEADERBOARD_INFO_URL, {leaderboardId}), options, priority) + const categories = async (priority = PRIORITY.FG_LOW, options = {}) => + fetchJson(CATEGORIES_URL, options, priority); + const ranking = async ( + category = "overall", + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + substituteVars(RANKING_URL, { category, page }), + options, + priority, + ); + const scores = async ( + playerId, + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + substituteVars(PLAYER_SCORES_URL, { playerId, page }), + options, + priority, + ); + const playerRankHistory = async ( + playerId, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + substituteVars(PLAYER_RANK_HISTORY, { playerId }), + options, + priority, + ); + const leaderboard = async ( + leaderboardId, + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + substituteVars(LEADERBOARD_URL, { leaderboardId, page }), + options, + priority, + ); + const leaderboardInfo = async ( + leaderboardId, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + substituteVars(LEADERBOARD_INFO_URL, { leaderboardId }), + options, + priority, + ); return { categories, @@ -29,5 +79,5 @@ export default (options = {}) => { leaderboard, leaderboardInfo, ...queueToReturn, - } -} \ No newline at end of file + }; +}; diff --git a/src/network/queues/beatmaps/api-queue.js b/src/network/queues/beatmaps/api-queue.js index 98a5630..5ab9f66 100644 --- a/src/network/queues/beatmaps/api-queue.js +++ b/src/network/queues/beatmaps/api-queue.js @@ -1,21 +1,23 @@ -import {default as createQueue, PRIORITY} from '../http-queue'; -import {substituteVars} from "../../../utils/format"; +import { default as createQueue, PRIORITY } from "../http-queue"; +import { substituteVars } from "../../../utils/format"; -const BEATMAPS_API_URL = 'https://api.beatsaver.com/'; -const SONG_BY_HASH_URL = BEATMAPS_API_URL + '/maps/hash/${hash}'; -const SONG_BY_KEY_URL = BEATMAPS_API_URL + '/maps/id/${key}' +const BEATMAPS_API_URL = "https://api.beatsaver.com/"; +const SONG_BY_HASH_URL = BEATMAPS_API_URL + "/maps/hash/${hash}"; +const SONG_BY_KEY_URL = BEATMAPS_API_URL + "/maps/id/${key}"; export default (options = {}) => { const queue = createQueue(options); - const {fetchJson, fetchHtml, ...queueToReturn} = queue; + const { fetchJson, fetchHtml, ...queueToReturn } = queue; - const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_HASH_URL, {hash}), options, priority) - const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SONG_BY_KEY_URL, {key}), options, priority) + const byHash = async (hash, priority = PRIORITY.FG_LOW, options = {}) => + fetchJson(substituteVars(SONG_BY_HASH_URL, { hash }), options, priority); + const byKey = async (key, priority = PRIORITY.FG_LOW, options = {}) => + fetchJson(substituteVars(SONG_BY_KEY_URL, { key }), options, priority); return { byHash, byKey, ...queueToReturn, - } -} \ No newline at end of file + }; +}; diff --git a/src/network/queues/http-queue.js b/src/network/queues/http-queue.js index b8d7ab5..0f9214b 100644 --- a/src/network/queues/http-queue.js +++ b/src/network/queues/http-queue.js @@ -1,9 +1,17 @@ -import {default as createQueue, PRIORITY as QUEUE_PRIORITY} from '../../utils/queue'; -import {SsrError, SsrTimeoutError} from '../../others/errors' -import {SsrHttpRateLimitError, SsrHttpResponseError, SsrNetworkError, SsrNetworkTimeoutError} from '../errors' -import {fetchHtml, fetchJson} from '../fetch'; -import makePendingPromisePool from '../../utils/pending-promises' -import {AbortError} from '../../utils/promise' +import { + default as createQueue, + PRIORITY as QUEUE_PRIORITY, +} from "../../utils/queue"; +import { SsrError, SsrTimeoutError } from "../../others/errors"; +import { + SsrHttpRateLimitError, + SsrHttpResponseError, + SsrNetworkError, + SsrNetworkTimeoutError, +} from "../errors"; +import { fetchHtml, fetchJson } from "../fetch"; +import makePendingPromisePool from "../../utils/pending-promises"; +import { AbortError } from "../../utils/promise"; const DEFAULT_RETRIES = 2; @@ -13,65 +21,91 @@ export const PRIORITY = { BG_HIGH: QUEUE_PRIORITY.NORMAL, BG_NORMAL: QUEUE_PRIORITY.LOW, BG_LOW: QUEUE_PRIORITY.LOWEST, -} +}; const resolvePromiseOrWaitForPending = makePendingPromisePool(); export default (options = {}) => { - const {retries, rateLimitTick, ...queueOptions} = {retries: DEFAULT_RETRIES, rateLimitTick: 500, ...options}; + const { retries, rateLimitTick, ...queueOptions } = { + retries: DEFAULT_RETRIES, + rateLimitTick: 500, + ...options, + }; const queue = createQueue(queueOptions); - const {add, emitter, ...queueToReturn} = queue; + const { add, emitter, ...queueToReturn } = queue; let lastRateLimitError = null; let rateLimitTimerId = null; - let currentRateLimit = {waiting: 0, remaining: null, limit: null, resetAt: null}; + let currentRateLimit = { + waiting: 0, + remaining: null, + limit: null, + resetAt: null, + }; const rateLimitTicker = () => { - const expiresInMs = lastRateLimitError && lastRateLimitError.resetAt ? lastRateLimitError.resetAt - new Date() + 1000 : 0; + const expiresInMs = + lastRateLimitError && lastRateLimitError.resetAt + ? lastRateLimitError.resetAt - new Date() + 1000 + : 0; if (expiresInMs <= 0) { - emitter.emit('waiting', {waiting: 0, remaining: null, limit: null, resetAt: null}); + emitter.emit("waiting", { + waiting: 0, + remaining: null, + limit: null, + resetAt: null, + }); if (rateLimitTimerId) clearTimeout(rateLimitTimerId); return; } - const {remaining, limit, resetAt} = lastRateLimitError; - emitter.emit('waiting', {waiting: expiresInMs, remaining, limit, resetAt}); + const { remaining, limit, resetAt } = lastRateLimitError; + emitter.emit("waiting", { + waiting: expiresInMs, + remaining, + limit, + resetAt, + }); if (rateLimitTimerId) clearTimeout(rateLimitTimerId); rateLimitTimerId = setTimeout(rateLimitTicker, rateLimitTick); - } + }; - const retriedFetch = async (fetchFunc, url, options, priority = PRIORITY.FG_LOW) => { + const retriedFetch = async ( + fetchFunc, + url, + options, + priority = PRIORITY.FG_LOW, + ) => { for (let i = 0; i <= retries; i++) { try { return await add(async () => { - if (lastRateLimitError) { - await lastRateLimitError.waitBeforeRetry(); + if (lastRateLimitError) { + await lastRateLimitError.waitBeforeRetry(); - lastRateLimitError = null; - } + lastRateLimitError = null; + } - return fetchFunc(url, options) - .then(response => { - currentRateLimit = {...response.rateLimit, waiting: 0}; + return fetchFunc(url, options) + .then((response) => { + currentRateLimit = { ...response.rateLimit, waiting: 0 }; - return response; - }) - .catch(err => { - if (err instanceof SsrTimeoutError) throw new SsrNetworkTimeoutError(err.timeout); + return response; + }) + .catch((err) => { + if (err instanceof SsrTimeoutError) + throw new SsrNetworkTimeoutError(err.timeout); - throw err; - }) - }, - priority, - ) + throw err; + }); + }, priority); } catch (err) { if (err instanceof SsrHttpResponseError) { - const {remaining, limit, resetAt} = err; - currentRateLimit = {waiting: 0, remaining, limit, resetAt}; + const { remaining, limit, resetAt } = err; + currentRateLimit = { waiting: 0, remaining, limit, resetAt }; } if (err instanceof SsrNetworkError) { @@ -79,7 +113,13 @@ export default (options = {}) => { if (!shouldRetry || i === retries) throw err; if (err instanceof SsrHttpRateLimitError) { - if (err.remaining <= 0 && err.resetAt && (!lastRateLimitError || !lastRateLimitError.resetAt || lastRateLimitError.resetAt < err.resetAt)) { + if ( + err.remaining <= 0 && + err.resetAt && + (!lastRateLimitError || + !lastRateLimitError.resetAt || + lastRateLimitError.resetAt < err.resetAt) + ) { lastRateLimitError = err; rateLimitTicker(); @@ -95,11 +135,17 @@ export default (options = {}) => { } } - throw new SsrError('Unknown error'); - } + throw new SsrError("Unknown error"); + }; - const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchJson, url, options, priority)); - const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) => resolvePromiseOrWaitForPending(url, () => retriedFetch(fetchHtml, url, options, priority)); + const queuedFetchJson = async (url, options, priority = PRIORITY.FG_LOW) => + resolvePromiseOrWaitForPending(url, () => + retriedFetch(fetchJson, url, options, priority), + ); + const queuedFetchHtml = async (url, options, priority = PRIORITY.FG_LOW) => + resolvePromiseOrWaitForPending(url, () => + retriedFetch(fetchHtml, url, options, priority), + ); const getRateLimit = () => currentRateLimit; @@ -108,5 +154,5 @@ export default (options = {}) => { fetchHtml: queuedFetchHtml, getRateLimit, ...queueToReturn, - } -} \ No newline at end of file + }; +}; diff --git a/src/network/queues/queues.js b/src/network/queues/queues.js index 37506fb..7ffa958 100644 --- a/src/network/queues/queues.js +++ b/src/network/queues/queues.js @@ -1,55 +1,99 @@ -import {writable} from 'svelte/store' -import {PRIORITY} from './http-queue' -import createScoreSaberApiQueue from './scoresaber/api-queue' -import createScoreSaberPageQueue from './scoresaber/page-queue' -import createBeatMapsApiQueue from './beatmaps/api-queue' -import createBeatSaviorApiQueue from './beatsavior/api-queue' -import createTwitchApiQueue from './twitch/api-queue' -import createAccSaberApiQueue from './accsaber/api-queue' +import { writable } from "svelte/store"; +import { PRIORITY } from "./http-queue"; +import createScoreSaberApiQueue from "./scoresaber/api-queue"; +import createScoreSaberPageQueue from "./scoresaber/page-queue"; +import createBeatMapsApiQueue from "./beatmaps/api-queue"; +import createBeatSaviorApiQueue from "./beatsavior/api-queue"; +import createTwitchApiQueue from "./twitch/api-queue"; +import createAccSaberApiQueue from "./accsaber/api-queue"; -export const getResponseBody = response => response ? response.body : null; -export const isResponseCached = response => !!(response && response.cached) -export const updateResponseBody = (response, body) => response ? {...response, body} : null; +export const getResponseBody = (response) => (response ? response.body : null); +export const isResponseCached = (response) => !!(response && response.cached); +export const updateResponseBody = (response, body) => + response ? { ...response, body } : null; -const initQueue = queue => { +const initQueue = (queue) => { let queueState = { size: 0, pending: 0, - rateLimit: {waiting: 0, remaining: null, limit: null, resetAt: null}, - progress: {num: 0, count: 0, progress: 1}, + rateLimit: { waiting: 0, remaining: null, limit: null, resetAt: null }, + progress: { num: 0, count: 0, progress: 1 }, }; - const {subscribe, set} = writable(queueState); + const { subscribe, set } = writable(queueState); - queue.on('change', ({size, pending}) => { - const {rateLimit: {waiting}} = queueState; - const {remaining, limit, resetAt} = queue.getRateLimit(); - queueState = {...queueState, size, pending, rateLimit: {waiting, remaining, limit, resetAt}}; + queue.on("change", ({ size, pending }) => { + const { + rateLimit: { waiting }, + } = queueState; + const { remaining, limit, resetAt } = queue.getRateLimit(); + queueState = { + ...queueState, + size, + pending, + rateLimit: { waiting, remaining, limit, resetAt }, + }; set(queueState); }); - queue.on('progress', ({progress, num, count}) => { - const {rateLimit: {waiting}} = queueState; - const {remaining, limit, resetAt} = queue.getRateLimit(); - queueState = {...queueState, progress: {num, count, progress}, rateLimit: {waiting, remaining, limit, resetAt}} + queue.on("progress", ({ progress, num, count }) => { + const { + rateLimit: { waiting }, + } = queueState; + const { remaining, limit, resetAt } = queue.getRateLimit(); + queueState = { + ...queueState, + progress: { num, count, progress }, + rateLimit: { waiting, remaining, limit, resetAt }, + }; set(queueState); }); - queue.on('waiting', ({waiting, remaining, limit, resetAt}) => { - queueState = {...queueState, rateLimit: {waiting, remaining, limit, resetAt}} + queue.on("waiting", ({ waiting, remaining, limit, resetAt }) => { + queueState = { + ...queueState, + rateLimit: { waiting, remaining, limit, resetAt }, + }; set(queueState); - }) + }); return { subscribe, ...queue, - } -} + }; +}; export default { - SCORESABER_API: initQueue(createScoreSaberApiQueue({concurrency: 3, timeout: 95000})), - SCORESABER_PAGE: initQueue(createScoreSaberPageQueue({concurrency: 3, timeout: 30000})), - BEATMAPS: initQueue(createBeatMapsApiQueue({concurrency: 1, timeout: 10000, intervalCap: 10, interval: 1000})), - BEATSAVIOR: initQueue(createBeatSaviorApiQueue({concurrency: 1, timeout: 10000, intervalCap: 60, interval: 60000})), - TWITCH: initQueue(createTwitchApiQueue({concurrency: 8, timeout: 8000, intervalCap: 800, interval: 60000})), - ACCSABER: initQueue(createAccSaberApiQueue({concurrency: 2, timeout: 10000})), + SCORESABER_API: initQueue( + createScoreSaberApiQueue({ concurrency: 3, timeout: 95000 }), + ), + SCORESABER_PAGE: initQueue( + createScoreSaberPageQueue({ concurrency: 3, timeout: 30000 }), + ), + BEATMAPS: initQueue( + createBeatMapsApiQueue({ + concurrency: 1, + timeout: 10000, + intervalCap: 10, + interval: 1000, + }), + ), + BEATSAVIOR: initQueue( + createBeatSaviorApiQueue({ + concurrency: 1, + timeout: 10000, + intervalCap: 60, + interval: 60000, + }), + ), + TWITCH: initQueue( + createTwitchApiQueue({ + concurrency: 8, + timeout: 8000, + intervalCap: 800, + interval: 60000, + }), + ), + ACCSABER: initQueue( + createAccSaberApiQueue({ concurrency: 2, timeout: 10000 }), + ), PRIORITY, -} \ No newline at end of file +}; diff --git a/src/network/queues/scoresaber/api-queue.js b/src/network/queues/scoresaber/api-queue.js index 97ef174..b400532 100644 --- a/src/network/queues/scoresaber/api-queue.js +++ b/src/network/queues/scoresaber/api-queue.js @@ -1,35 +1,79 @@ -import {default as createQueue, PRIORITY} from '../http-queue'; -import {substituteVars} from '../../../utils/format' -import {PLAYER_SCORES_PER_PAGE, PLAYERS_PER_PAGE} from '../../../utils/scoresaber/consts' +import { default as createQueue, PRIORITY } from "../http-queue"; +import { substituteVars } from "../../../utils/format"; +import { + PLAYER_SCORES_PER_PAGE, + PLAYERS_PER_PAGE, +} from "../../../utils/scoresaber/consts"; -export const SS_API_HOST = 'https://new.scoresaber.com'; +export const SS_API_HOST = "https://new.scoresaber.com"; export const SS_API_URL = `${SS_API_HOST}/api`; -export const SS_API_PLAYER_INFO_URL = SS_API_URL + '/player/${playerId}/full'; -export const SS_API_RECENT_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/recent/${page}'; -export const SS_API_TOP_SCORES_URL = SS_API_URL + '/player/${playerId}/scores/top/${page}'; -export const SS_API_FIND_PLAYER_URL = SS_API_URL + '/players/by-name/${query}' -export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + '/players/${page}' -export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + '/players/pages' +export const SS_API_PLAYER_INFO_URL = SS_API_URL + "/player/${playerId}/full"; +export const SS_API_RECENT_SCORES_URL = + SS_API_URL + "/player/${playerId}/scores/recent/${page}"; +export const SS_API_TOP_SCORES_URL = + SS_API_URL + "/player/${playerId}/scores/top/${page}"; +export const SS_API_FIND_PLAYER_URL = SS_API_URL + "/players/by-name/${query}"; +export const SS_API_RANKING_GLOBAL_URL = SS_API_URL + "/players/${page}"; +export const SS_API_RANKING_GLOBAL_PAGES_URL = SS_API_URL + "/players/pages"; export default (options = {}) => { const queue = createQueue(options); - const {fetchJson, fetchHtml, ...queueToReturn} = queue; + const { fetchJson, fetchHtml, ...queueToReturn } = queue; - const fetchScores = async (baseUrl, playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(baseUrl, {playerId, page}), options, priority); + const fetchScores = async ( + baseUrl, + playerId, + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson(substituteVars(baseUrl, { playerId, page }), options, priority); - const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_PLAYER_INFO_URL, {playerId}), options, priority); + const player = async (playerId, priority = PRIORITY.FG_LOW, options = {}) => + fetchJson( + substituteVars(SS_API_PLAYER_INFO_URL, { playerId }), + options, + priority, + ); - const recentScores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchScores(SS_API_RECENT_SCORES_URL, playerId, page, priority, options); + const recentScores = async ( + playerId, + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => fetchScores(SS_API_RECENT_SCORES_URL, playerId, page, priority, options); - const topScores = async (playerId, page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchScores(SS_API_TOP_SCORES_URL, playerId, page, priority, options); + const topScores = async ( + playerId, + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => fetchScores(SS_API_TOP_SCORES_URL, playerId, page, priority, options); - const findPlayer = async (query, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_FIND_PLAYER_URL, {query: encodeURIComponent(query)}), options, priority); + const findPlayer = async (query, priority = PRIORITY.FG_LOW, options = {}) => + fetchJson( + substituteVars(SS_API_FIND_PLAYER_URL, { + query: encodeURIComponent(query), + }), + options, + priority, + ); - const rankingGlobal = async (page = 1, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(substituteVars(SS_API_RANKING_GLOBAL_URL, {page}), options, priority); + const rankingGlobal = async ( + page = 1, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + substituteVars(SS_API_RANKING_GLOBAL_URL, { page }), + options, + priority, + ); - const rankingGlobalPages = async (priority = PRIORITY.FG_LOW, options = {}) => fetchJson(SS_API_RANKING_GLOBAL_PAGES_URL, options, priority); + const rankingGlobalPages = async (priority = PRIORITY.FG_LOW, options = {}) => + fetchJson(SS_API_RANKING_GLOBAL_PAGES_URL, options, priority); return { player, @@ -42,5 +86,5 @@ export default (options = {}) => { PLAYER_SCORES_PER_PAGE, PLAYERS_PER_PAGE, ...queueToReturn, - } -} \ No newline at end of file + }; +}; diff --git a/src/network/queues/scoresaber/page-queue.js b/src/network/queues/scoresaber/page-queue.js index ed097e5..b7104ea 100644 --- a/src/network/queues/scoresaber/page-queue.js +++ b/src/network/queues/scoresaber/page-queue.js @@ -24,7 +24,7 @@ export const parseSsInt = (text) => { export const parseSsFloat = (text) => text ? parseFloat( - getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, "")) + getFirstRegexpMatch(/([0-9,.]+)\s*$/, text.replace(/[^\d.]/g, "")), ) : null; @@ -78,32 +78,32 @@ export default (options = {}) => { const rankeds = async ( page = 1, priority = PRIORITY.BG_NORMAL, - options = {} + options = {}, ) => fetchJson(substituteVars(RANKEDS_URL, { page }), options, priority).then( (r) => { r.body = processRankeds(r.body); return r; - } + }, ); const processPlayerProfile = (playerId, doc) => { cfDecryptEmail(doc); let avatar = getImgUrl( - opt(doc.querySelector(".column.avatar img"), "src", null) + opt(doc.querySelector(".column.avatar img"), "src", null), ); let playerName = opt( doc.querySelector(".content .column:not(.avatar) .title a"), - "innerText" + "innerText", ); playerName = playerName ? playerName.trim() : null; let country = getFirstRegexpMatch( /^.*?\/flags\/([^.]+)\..*$/, - opt(doc.querySelector(".content .column .title img"), "src") + opt(doc.querySelector(".content .column .title img"), "src"), ); country = country ? country.toUpperCase() : null; @@ -111,8 +111,8 @@ export default (options = {}) => { opt( doc.querySelector(".pagination .pagination-list li a.is-current"), "innerText", - null - ) + null, + ), ); pageNum = !isNaN(pageNum) ? pageNum : null; @@ -120,8 +120,8 @@ export default (options = {}) => { opt( doc.querySelector(".pagination .pagination-list li:last-of-type"), "innerText", - null - ) + null, + ), ); pageQty = !isNaN(pageQty) ? pageQty : null; @@ -130,31 +130,31 @@ export default (options = {}) => { /^\s*(?:[^:]+)\s*:?\s*<\/strong>\s*(.*)$/, opt( doc.querySelector( - ".columns .column:not(.is-narrow) ul li:nth-of-type(3)" + ".columns .column:not(.is-narrow) ul li:nth-of-type(3)", ), - "innerHTML" - ) - ) + "innerHTML", + ), + ), ); totalItems = !isNaN(totalItems) ? totalItems : 0; let playerRank = parseSsInt( opt( doc.querySelector( - ".content .column ul li:first-of-type a:first-of-type" + ".content .column ul li:first-of-type a:first-of-type", ), - "innerText" - ) + "innerText", + ), ); playerRank = !isNaN(playerRank) ? playerRank : null; let countryRank = parseSsInt( opt( doc.querySelector( - '.content .column ul li:first-of-type a[href^="/global?country="]' + '.content .column ul li:first-of-type a[href^="/global?country="]', ), - "innerText" - ) + "innerText", + ), ); countryRank = !isNaN(countryRank) ? countryRank : null; @@ -170,7 +170,7 @@ export default (options = {}) => { [...doc.querySelectorAll(".content .column ul li")] .map((li) => { const matches = li.innerHTML.match( - /^\s*([^:]+)\s*:?\s*<\/strong>\s*(.*)$/ + /^\s*([^:]+)\s*:?\s*<\/strong>\s*(.*)$/, ); if (!matches) return null; @@ -219,7 +219,7 @@ export default (options = {}) => { const item = mapping.find((m) => m.key === matches[1]); return item ? { ...item, value } : { label: matches[1], value }; }) - .filter((s) => s) + .filter((s) => s), ) .reduce( (cum, item) => { @@ -255,7 +255,7 @@ export default (options = {}) => { return cum; }, - { inactiveAccount: false, bannedAccount: false } + { inactiveAccount: false, bannedAccount: false }, ); const scores = [...doc.querySelectorAll("table.ranking tbody tr")].map( @@ -274,7 +274,7 @@ export default (options = {}) => { if (song) { const leaderboardId = parseInt( getFirstRegexpMatch(/leaderboard\/(\d+)/, song.href), - 10 + 10, ); ret.leaderboardId = leaderboardId ? leaderboardId : null; } else { @@ -293,7 +293,7 @@ export default (options = {}) => { .replace(/&/g, "&") .replace( /\[email protected]<\/span>/g, - "" + "", ) .match(/^(.*?)\s*]+>(.*?)<\/span>/) : null; @@ -328,7 +328,7 @@ export default (options = {}) => { ret.timeSet = songDate ? dateFromString(songDate.title) : null; const pp = parseSsFloat( - opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText") + opt(tr.querySelector("th.score .scoreTop.ppValue"), "innerText"), ); ret.pp = !isNaN(pp) ? pp : null; @@ -337,9 +337,9 @@ export default (options = {}) => { /^\(([0-9.]+)pp\)$/, opt( tr.querySelector("th.score .scoreTop.ppWeightedValue"), - "innerText" - ) - ) + "innerText", + ), + ), ); ret.ppWeighted = !isNaN(ppWeighted) ? ppWeighted : null; @@ -380,7 +380,7 @@ export default (options = {}) => { } return ret; - } + }, ); const recentPlay = scores && scores.length && scores[0].timeSet ? scores[0].timeSet : null; @@ -394,18 +394,18 @@ export default (options = {}) => { externalProfileUrl: opt( doc.querySelector(".content .column:not(.avatar) .title a"), "href", - null + null, ), history: getFirstRegexpMatch( /data:\s*\[([0-9,]+)\]/, - doc.body.innerHTML + doc.body.innerHTML, ), country, badges: [...doc.querySelectorAll(".column.avatar center img")].map( (img) => ({ image: getImgUrl(img.src), description: img.title, - }) + }), ), rank: stats.rank ? stats.rank : null, countryRank: stats.countryRank ? stats.countryRank : null, @@ -435,7 +435,7 @@ export default (options = {}) => { fetchHtml( substituteVars(PLAYER_PROFILE_URL, { playerId }), options, - priority + priority, ).then((r) => { r.body = processPlayerProfile(playerId, r.body); @@ -451,17 +451,17 @@ export default (options = {}) => { const id = getFirstRegexpMatch(/\/(\d+)$/, a.href); const avatar = getImgUrl( - opt(tr.querySelector("td.picture img"), "src", null) + opt(tr.querySelector("td.picture img"), "src", null), ); let country = getFirstRegexpMatch( /^.*?\/flags\/([^.]+)\..*$/, - opt(tr.querySelector("td.player img"), "src", null) + opt(tr.querySelector("td.player img"), "src", null), ); country = country ? country.toUpperCase() : null; let difference = parseSsInt( - opt(tr.querySelector("td.diff"), "innerText", null) + opt(tr.querySelector("td.diff"), "innerText", null), ); difference = !isNaN(difference) ? difference : null; @@ -469,15 +469,15 @@ export default (options = {}) => { playerName = playerName || playerName === "" ? playerName.trim() : null; let pp = parseSsFloat( - opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText") + opt(tr.querySelector("td.pp .scoreTop.ppValue"), "innerText"), ); pp = !isNaN(pp) ? pp : null; let rank = parseSsInt( getFirstRegexpMatch( /^\s*#(\d+)\s*$/, - opt(tr.querySelector("td.rank"), "innerText", null) - ) + opt(tr.querySelector("td.rank"), "innerText", null), + ), ); rank = !isNaN(rank) ? rank : null; @@ -491,7 +491,7 @@ export default (options = {}) => { pp, rank, }; - } + }, ); return { players: data }; @@ -501,12 +501,12 @@ export default (options = {}) => { country, page = 1, priority = PRIORITY.FG_LOW, - options = {} + options = {}, ) => fetchHtml( substituteVars(COUNTRY_RANKING_URL, { country, page }), options, - priority + priority, ).then((r) => { r.body = processCountryRanking(country, r.body); @@ -529,11 +529,11 @@ export default (options = {}) => { }; ret.player.playerInfo.avatar = getImgUrl( - opt(tr.querySelector(".picture img"), "src", null) + opt(tr.querySelector(".picture img"), "src", null), ); ret.score.rank = parseSsInt( - opt(tr.querySelector("td.rank"), "innerText") + opt(tr.querySelector("td.rank"), "innerText"), ); if (isNaN(ret.score.rank)) ret.score.rank = null; @@ -541,7 +541,7 @@ export default (options = {}) => { if (player) { let country = getFirstRegexpMatch( /^.*?\/flags\/([^.]+)\..*$/, - opt(player.querySelector("img"), "src", "") + opt(player.querySelector("img"), "src", ""), ); country = country ? country.toUpperCase() : null; if (country) { @@ -551,14 +551,14 @@ export default (options = {}) => { ret.player.name = opt( player.querySelector("span.songTop.pp"), - "innerText" + "innerText", ); ret.player.name = ret.player.name ? ret.player.name.trim().replace("'", "'") : null; ret.player.playerId = getFirstRegexpMatch( /\/u\/(\d+)((\?|&|#).*)?$/, - opt(player, "href", "") + opt(player, "href", ""), ); ret.player.playerId = ret.player.playerId ? ret.player.playerId.trim() @@ -574,7 +574,7 @@ export default (options = {}) => { ret.score.timeSetString = opt( tr.querySelector("td.timeset"), "innerText", - null + null, ); if (ret.score.timeSetString) ret.score.timeSetString = ret.score.timeSetString.trim(); @@ -602,7 +602,7 @@ export default (options = {}) => { const diffs = [...doc.querySelectorAll(".tabs ul li a")].map((a) => { let leaderboardId = parseInt( getFirstRegexpMatch(/leaderboard\/(\d+)$/, a.href), - 10 + 10, ); if (isNaN(leaderboardId)) leaderboardId = null; @@ -615,7 +615,7 @@ export default (options = {}) => { const currentDiffHuman = opt( doc.querySelector(".tabs li.is-active a span"), "innerText", - null + null, ); let diff = null; @@ -628,20 +628,20 @@ export default (options = {}) => { const songName = opt( doc.querySelector( - ".column.is-one-third-desktop .box:first-of-type .title a" + ".column.is-one-third-desktop .box:first-of-type .title a", ), "innerText", - null + null, ); const imageUrl = getImgUrl( opt( doc.querySelector( - ".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img" + ".column.is-one-third-desktop .box:first-of-type .columns .column.is-one-quarter img", ), "src", - null - ) + null, + ), ); const songInfo = [ @@ -656,13 +656,13 @@ export default (options = {}) => { ] .map((sid) => { let songInfoBox = doc.querySelector( - ".column.is-one-third-desktop .box:first-of-type" + ".column.is-one-third-desktop .box:first-of-type", ); return { ...sid, value: songInfoBox ? songInfoBox.innerHTML.match( - new RegExp(sid.label + ":\\s*(.*?)", "i") + new RegExp(sid.label + ":\\s*(.*?)", "i"), ) : null, }; @@ -708,7 +708,7 @@ export default (options = {}) => { return cum; }, - { imageUrl, stats: {} } + { imageUrl, stats: {} }, ); const { stats, ...song } = songInfo; @@ -718,9 +718,9 @@ export default (options = {}) => { opt( doc.querySelector(".pagination .pagination-list li:last-of-type"), "innerText", - null + null, ), - 10 + 10, ); if (isNaN(pageQty)) pageQty = null; @@ -736,7 +736,7 @@ export default (options = {}) => { let diffChartText = getFirstRegexpMatch( /'difficulty',\s*([0-9.,\s]+)\s*\]/, - doc.body.innerHTML + doc.body.innerHTML, ); let diffChart = (diffChartText ? diffChartText : "") .split(",") @@ -758,12 +758,12 @@ export default (options = {}) => { leaderboardId, page = 1, priority = PRIORITY.FG_LOW, - options = {} + options = {}, ) => fetchHtml( substituteVars(LEADERBOARD_URL, { leaderboardId, page }), options, - priority + priority, ).then((r) => { r.body = processLeaderboard(leaderboardId, page, r.body); diff --git a/src/network/queues/twitch/api-queue.js b/src/network/queues/twitch/api-queue.js index 7aa69df..bd495f7 100644 --- a/src/network/queues/twitch/api-queue.js +++ b/src/network/queues/twitch/api-queue.js @@ -1,44 +1,104 @@ -import {default as createQueue, PRIORITY} from '../http-queue'; -import ssrConfig from '../../../ssr-config' -import {substituteVars} from "../../../utils/format"; +import { default as createQueue, PRIORITY } from "../http-queue"; +import ssrConfig from "../../../ssr-config"; +import { substituteVars } from "../../../utils/format"; -const CLIENT_ID = 'u0swxz56n4iumc634at1osoqdk31qt'; +const CLIENT_ID = "u0swxz56n4iumc634at1osoqdk31qt"; -const TWITCH_AUTH_URL = 'https://id.twitch.tv/oauth2' -const AUTHORIZATION_URL = `${TWITCH_AUTH_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(ssrConfig.domain + '/twitch')}&response_type=token` + '&scope=${scopes}&state=${state}'; -const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate` +const TWITCH_AUTH_URL = "https://id.twitch.tv/oauth2"; +const AUTHORIZATION_URL = + `${TWITCH_AUTH_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent( + ssrConfig.domain + "/twitch", + )}&response_type=token` + "&scope=${scopes}&state=${state}"; +const VALIDATE_URL = `${TWITCH_AUTH_URL}/validate`; -const TWITCH_API_URL = 'https://api.twitch.tv/helix'; -const PROFILE_URL = TWITCH_API_URL + '/users?login=${login}'; -const VIDEOS_URL = TWITCH_API_URL + '/videos?user_id=${userId}&type=${type}&first=100'; -const STREAMS_URL = TWITCH_API_URL + '/streams?user_id=${userId}'; +const TWITCH_API_URL = "https://api.twitch.tv/helix"; +const PROFILE_URL = TWITCH_API_URL + "/users?login=${login}"; +const VIDEOS_URL = + TWITCH_API_URL + "/videos?user_id=${userId}&type=${type}&first=100"; +const STREAMS_URL = TWITCH_API_URL + "/streams?user_id=${userId}"; export default (options = {}) => { const queue = createQueue(options); - const {fetchJson, fetchHtml, ...queueToReturn} = queue; + const { fetchJson, fetchHtml, ...queueToReturn } = queue; - const fetchApi = (url, accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson( + const fetchApi = ( url, - { - ...options, - headers: { - 'Client-ID': CLIENT_ID, - 'Authorization': `Bearer ${accessToken}` - } - }, - priority, - ) + accessToken, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + url, + { + ...options, + headers: { + "Client-ID": CLIENT_ID, + Authorization: `Bearer ${accessToken}`, + }, + }, + priority, + ); - const getAuthUrl = (state = '', scopes = '') => substituteVars(AUTHORIZATION_URL, {state: encodeURIComponent(state), scopes: encodeURIComponent(scopes)}); + const getAuthUrl = (state = "", scopes = "") => + substituteVars(AUTHORIZATION_URL, { + state: encodeURIComponent(state), + scopes: encodeURIComponent(scopes), + }); - const validateToken = async (accessToken, priority = PRIORITY.FG_LOW, options = {}) => fetchJson(VALIDATE_URL, {...options, headers: {'Authorization': `OAuth ${accessToken}`}}, priority) + const validateToken = async ( + accessToken, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchJson( + VALIDATE_URL, + { ...options, headers: { Authorization: `OAuth ${accessToken}` } }, + priority, + ); - const profile = async (accessToken, login, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(PROFILE_URL, {login: encodeURIComponent(login)}), accessToken, priority, options) + const profile = async ( + accessToken, + login, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchApi( + substituteVars(PROFILE_URL, { login: encodeURIComponent(login) }), + accessToken, + priority, + options, + ); - const videos = async (accessToken, userId, type = 'archive', priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(VIDEOS_URL, {userId: encodeURIComponent(userId), type: encodeURIComponent(type)}), accessToken, priority, options) + const videos = async ( + accessToken, + userId, + type = "archive", + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchApi( + substituteVars(VIDEOS_URL, { + userId: encodeURIComponent(userId), + type: encodeURIComponent(type), + }), + accessToken, + priority, + options, + ); - const streams = async (accessToken, userId, priority = PRIORITY.FG_LOW, options = {}) => fetchApi(substituteVars(STREAMS_URL, {userId: encodeURIComponent(userId)}), accessToken, priority, options) + const streams = async ( + accessToken, + userId, + priority = PRIORITY.FG_LOW, + options = {}, + ) => + fetchApi( + substituteVars(STREAMS_URL, { userId: encodeURIComponent(userId) }), + accessToken, + priority, + options, + ); return { getAuthUrl, @@ -47,5 +107,5 @@ export default (options = {}) => { videos, streams, ...queueToReturn, - } -} \ No newline at end of file + }; +}; diff --git a/src/network/utils.js b/src/network/utils.js index d4618b2..7ad0a2f 100644 --- a/src/network/utils.js +++ b/src/network/utils.js @@ -1,13 +1,13 @@ -export const parseRateLimitHeaders = response => { +export const parseRateLimitHeaders = (response) => { if (!response || !response.headers) return null; - const remaining = parseInt(response.headers.get('x-ratelimit-remaining'), 10); - const limit = parseInt(response.headers.get('x-ratelimit-limit'), 10); - const resetAt = parseInt(response.headers.get('x-ratelimit-reset'), 10); + const remaining = parseInt(response.headers.get("x-ratelimit-remaining"), 10); + const limit = parseInt(response.headers.get("x-ratelimit-limit"), 10); + const resetAt = parseInt(response.headers.get("x-ratelimit-reset"), 10); return { remaining: !isNaN(remaining) ? remaining : null, limit: !isNaN(limit) ? limit : null, resetAt: !isNaN(resetAt) ? new Date(resetAt * 1000) : null, - } -} \ No newline at end of file + }; +}; diff --git a/src/others/errors.js b/src/others/errors.js index b8dbed5..2b13351 100644 --- a/src/others/errors.js +++ b/src/others/errors.js @@ -12,7 +12,7 @@ export class SsrError extends Error { export class SsrTimeoutError extends SsrError { constructor(timeout, message) { - super(message && message.length ? message : `Timeout Error (${timeout}ms)`) + super(message && message.length ? message : `Timeout Error (${timeout}ms)`); this.name = "SsrTimeoutError"; this.timeout = timeout; @@ -21,9 +21,9 @@ export class SsrTimeoutError extends SsrError { export class SsrDataFormatError extends SsrError { constructor(message, previous = null) { - super(message && message.length ? message : `Data format error`) + super(message && message.length ? message : `Data format error`); this.name = "SsrDataFormatError"; this.previous = previous; } -} \ No newline at end of file +} diff --git a/src/services/accsaber.js b/src/services/accsaber.js index e0c5c85..2096ede 100644 --- a/src/services/accsaber.js +++ b/src/services/accsaber.js @@ -1,35 +1,36 @@ -import {db} from '../db/db' -import queues from '../network/queues/queues'; -import accSaberCategoriesApiClient from '../network/clients/accsaber/api-categories'; -import accSaberRankingApiClient from '../network/clients/accsaber/api-ranking'; -import accSaberScoresApiClient from '../network/clients/accsaber/api-scores'; -import accSaberPlayerRankHistoryApiClient from '../network/clients/accsaber/api-player-rank-history'; -import accSaberCategoriesRepository from '../db/repository/accsaber-categories' -import accSaberPlayersRepository from '../db/repository/accsaber-players' -import accSaberPlayersHistoryRepository from '../db/repository/accsaber-players-history'; -import keyValueRepository from '../db/repository/key-value' -import createPlayerService from '../services/scoresaber/player'; -import {capitalize, convertArrayToObjectByKey} from '../utils/js' -import log from '../utils/logger' +import { db } from "../db/db"; +import queues from "../network/queues/queues"; +import accSaberCategoriesApiClient from "../network/clients/accsaber/api-categories"; +import accSaberRankingApiClient from "../network/clients/accsaber/api-ranking"; +import accSaberScoresApiClient from "../network/clients/accsaber/api-scores"; +import accSaberPlayerRankHistoryApiClient from "../network/clients/accsaber/api-player-rank-history"; +import accSaberCategoriesRepository from "../db/repository/accsaber-categories"; +import accSaberPlayersRepository from "../db/repository/accsaber-players"; +import accSaberPlayersHistoryRepository from "../db/repository/accsaber-players-history"; +import keyValueRepository from "../db/repository/key-value"; +import createPlayerService from "../services/scoresaber/player"; +import { capitalize, convertArrayToObjectByKey } from "../utils/js"; +import log from "../utils/logger"; import { addToDate, toAccSaberMidnight, formatDate, HOUR, MINUTE, - dateFromString, truncateDate, -} from '../utils/date' -import {PRIORITY} from '../network/queues/http-queue' -import makePendingPromisePool from '../utils/pending-promises' -import {getServicePlayerGain, serviceFilterFunc} from './utils' -import {PLAYER_SCORES_PER_PAGE} from '../utils/accsaber/consts' -import {roundToPrecision} from '../utils/format' + dateFromString, + truncateDate, +} from "../utils/date"; +import { PRIORITY } from "../network/queues/http-queue"; +import makePendingPromisePool from "../utils/pending-promises"; +import { getServicePlayerGain, serviceFilterFunc } from "./utils"; +import { PLAYER_SCORES_PER_PAGE } from "../utils/accsaber/consts"; +import { roundToPrecision } from "../utils/format"; const REFRESH_INTERVAL = HOUR; const SCORES_NETWORK_TTL = MINUTE * 5; const HISTOGRAM_AP_PRECISION = 5; -const CATEGORIES_ORDER = ['overall', 'true', 'standard', 'tech']; +const CATEGORIES_ORDER = ["overall", "true", "standard", "tech"]; let service = null; export default () => { @@ -40,61 +41,120 @@ export default () => { const resolvePromiseOrWaitForPending = makePendingPromisePool(); const getCategories = async () => { - const categories = await resolvePromiseOrWaitForPending(`accSaberCategories`, () => accSaberCategoriesRepository().getAll()); + const categories = await resolvePromiseOrWaitForPending( + `accSaberCategories`, + () => accSaberCategoriesRepository().getAll(), + ); - const getIdx = category => { - const idx = CATEGORIES_ORDER.findIndex(v => v === category?.name); + const getIdx = (category) => { + const idx = CATEGORIES_ORDER.findIndex((v) => v === category?.name); return idx >= 0 ? idx : 100000; - } - return categories.sort((a,b) => getIdx(a) - getIdx(b)); - } + }; + return categories.sort((a, b) => getIdx(a) - getIdx(b)); + }; - const getPlayer = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () => accSaberPlayersRepository().getAllFromIndex('accsaber-players-playerId', playerId)); - const getRanking = async (category = 'overall') => accSaberPlayersRepository().getAllFromIndex('accsaber-players-category', category); - const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () => accSaberPlayersHistoryRepository().getAllFromIndex('accsaber-players-history-playerId', playerId)) + const getPlayer = async (playerId) => + resolvePromiseOrWaitForPending(`accSaberPlayer/${playerId}`, () => + accSaberPlayersRepository().getAllFromIndex( + "accsaber-players-playerId", + playerId, + ), + ); + const getRanking = async (category = "overall") => + accSaberPlayersRepository().getAllFromIndex( + "accsaber-players-category", + category, + ); + const getPlayerHistory = async (playerId) => + resolvePromiseOrWaitForPending(`accSaberPlayerHistory/${playerId}`, () => + accSaberPlayersHistoryRepository().getAllFromIndex( + "accsaber-players-history-playerId", + playerId, + ), + ); - const isDataForPlayerAvailable = async playerId => (await Promise.all([getPlayer(playerId), getCategories()])).every(d => d?.length) + const isDataForPlayerAvailable = async (playerId) => + (await Promise.all([getPlayer(playerId), getCategories()])).every( + (d) => d?.length, + ); - const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => getServicePlayerGain(playerHistory, toAccSaberMidnight, 'accSaberDate', daysAgo, maxDaysAgo); + const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => + getServicePlayerGain( + playerHistory, + toAccSaberMidnight, + "accSaberDate", + daysAgo, + maxDaysAgo, + ); - const getLastUpdatedKey = type => `accSaber${capitalize(type)}LastUpdated`; - const getLastUpdated = async (type = 'all') => keyValueRepository().get(getLastUpdatedKey(type)); - const setLastUpdated = async (type = 'all', date) => keyValueRepository().set(date, getLastUpdatedKey(type)); + const getLastUpdatedKey = (type) => `accSaber${capitalize(type)}LastUpdated`; + const getLastUpdated = async (type = "all") => + keyValueRepository().get(getLastUpdatedKey(type)); + const setLastUpdated = async (type = "all", date) => + keyValueRepository().set(date, getLastUpdatedKey(type)); - const shouldRefresh = async (type = 'all', forceUpdate = false) => { + const shouldRefresh = async (type = "all", forceUpdate = false) => { if (!forceUpdate) { const lastUpdated = await getLastUpdated(type); if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) { - log.debug(`Refresh interval for ${type} not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'AccSaberService') + log.debug( + `Refresh interval for ${type} not yet expired, skipping. Next refresh on ${formatDate( + addToDate(REFRESH_INTERVAL, lastUpdated), + )}`, + "AccSaberService", + ); return false; } } return true; - } + }; - const fetchScoresPage = async (playerId, page = 1, priority = PRIORITY.FG_LOW, {...options} = {}) => { + const fetchScoresPage = async ( + playerId, + page = 1, + priority = PRIORITY.FG_LOW, + { ...options } = {}, + ) => { if (!options) options = {}; - if (!options.hasOwnProperty('cacheTtl')) options.cacheTtl = SCORES_NETWORK_TTL; + if (!options.hasOwnProperty("cacheTtl")) + options.cacheTtl = SCORES_NETWORK_TTL; - const categoriesByDisplayName = convertArrayToObjectByKey(await getCategories(), 'displayName'); + const categoriesByDisplayName = convertArrayToObjectByKey( + await getCategories(), + "displayName", + ); - return (await resolvePromiseOrWaitForPending(`fetchPlayerScores/${playerId}/${page}`, () => accSaberScoresApiClient.getProcessed({...options, playerId, page, priority}))) - .map(s => ({ - ...s, - leaderboard: { - ...s?.leaderboard, - category: categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ?? null, - } - })) - } + return ( + await resolvePromiseOrWaitForPending( + `fetchPlayerScores/${playerId}/${page}`, + () => + accSaberScoresApiClient.getProcessed({ + ...options, + playerId, + page, + priority, + }), + ) + ).map((s) => ({ + ...s, + leaderboard: { + ...s?.leaderboard, + category: + categoriesByDisplayName[s?.leaderboard?.categoryDisplayName]?.name ?? + null, + }, + })); + }; - const getScoresHistogramDefinition = (serviceParams = {type: 'overall', sort: 'ap', order: 'desc'}) => { - const scoreType = serviceParams?.type ?? 'overall'; - const sort = serviceParams?.sort ?? 'ap'; - const order = serviceParams?.order ?? 'desc'; + const getScoresHistogramDefinition = ( + serviceParams = { type: "overall", sort: "ap", order: "desc" }, + ) => { + const scoreType = serviceParams?.type ?? "overall"; + const sort = serviceParams?.sort ?? "ap"; + const order = serviceParams?.order ?? "desc"; const commonFilterFunc = serviceFilterFunc(serviceParams); @@ -104,68 +164,75 @@ export default () => { let maxBucketSize = null; let bucketSizeStep = null; let bucketSizeValues = null; - let type = 'linear'; - let valFunc = s => s; - let filterFunc = s => commonFilterFunc(s) && (scoreType === 'overall' || s?.leaderboard?.category === scoreType); - let histogramFilterFunc = s => s; - let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear' - ? roundToPrecision(valFunc(s), precision) - : truncateDate(valFunc(s), precision); - let prefix = ''; - let prefixLong = ''; - let suffix = ''; - let suffixLong = ''; + let type = "linear"; + let valFunc = (s) => s; + let filterFunc = (s) => + commonFilterFunc(s) && + (scoreType === "overall" || s?.leaderboard?.category === scoreType); + let histogramFilterFunc = (s) => s; + let roundedValFunc = (s, type = type, precision = bucketSize) => + type === "linear" + ? roundToPrecision(valFunc(s), precision) + : truncateDate(valFunc(s), precision); + let prefix = ""; + let prefixLong = ""; + let suffix = ""; + let suffixLong = ""; - switch(sort) { - case 'ap': - valFunc = s => s?.ap; - type = 'linear'; + switch (sort) { + case "ap": + valFunc = (s) => s?.ap; + type = "linear"; bucketSize = HISTOGRAM_AP_PRECISION; minBucketSize = 1; maxBucketSize = 100; bucketSizeStep = 1; round = 0; - suffix = ' AP'; - suffixLong = ' AP'; + suffix = " AP"; + suffixLong = " AP"; break; - case 'recent': - valFunc = s => s?.timeSet; - type = 'time'; - bucketSize = 'day' + case "recent": + valFunc = (s) => s?.timeSet; + type = "time"; + bucketSize = "day"; break; - case 'acc': - valFunc = s => s?.acc; - type = 'linear'; + case "acc": + valFunc = (s) => s?.acc; + type = "linear"; bucketSize = 0.05; minBucketSize = 0.05; maxBucketSize = 1; bucketSizeStep = 0.05; round = 2; - suffix = '%'; - suffixLong = '%'; + suffix = "%"; + suffixLong = "%"; break; - case 'rank': - valFunc = s => s?.score?.rank; - type = 'linear'; + case "rank": + valFunc = (s) => s?.score?.rank; + type = "linear"; bucketSize = 5; minBucketSize = 1; maxBucketSize = 100; bucketSizeStep = 1; round = 0; - prefix = ''; - prefixLong = '#'; + prefix = ""; + prefixLong = "#"; break; } return { getValue: valFunc, - getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize), + getRoundedValue: + (bucketSize = bucketSize) => + (s) => + roundedValFunc(s, type, bucketSize), filter: filterFunc, histogramFilter: histogramFilterFunc, - sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a), + sort: (a, b) => + order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a), type, bucketSize, minBucketSize, @@ -177,164 +244,224 @@ export default () => { prefixLong, suffix, suffixLong, - order - } - } + order, + }; + }; - const getPlayerScores = async playerId => { + const getPlayerScores = async (playerId) => { try { return fetchScoresPage(playerId, 1); - } - catch (err) { + } catch (err) { return []; } - } + }; - const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => { + const getPlayerScoresPage = async ( + playerId, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + ) => { let page = serviceParams?.page ?? 1; if (page < 1) page = 1; let playerScores; try { playerScores = await fetchScoresPage(playerId, page); - } - catch (err) { - return {total: 0, scores: []}; + } catch (err) { + return { total: 0, scores: [] }; } - if (!playerScores?.length) return {total: 0, scores: []}; + if (!playerScores?.length) return { total: 0, scores: [] }; - const {sort: sortFunc, filter: filterFunc} = getScoresHistogramDefinition(serviceParams); + const { sort: sortFunc, filter: filterFunc } = + getScoresHistogramDefinition(serviceParams); - playerScores = playerScores.filter(filterFunc).sort(sortFunc) + playerScores = playerScores.filter(filterFunc).sort(sortFunc); const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE; - if (playerScores.length < startIdx + 1) return {total: 0, scores: []}; + if (playerScores.length < startIdx + 1) return { total: 0, scores: [] }; return { total: playerScores.length, itemsPerPage: PLAYER_SCORES_PER_PAGE, - scores: playerScores - .slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE) - } - } + scores: playerScores.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE), + }; + }; - const fetchPlayerRankHistory = async (playerId, priority = PRIORITY.FG_LOW, {...options} = {}) => { + const fetchPlayerRankHistory = async ( + playerId, + priority = PRIORITY.FG_LOW, + { ...options } = {}, + ) => { if (!options) options = {}; - if (!options.hasOwnProperty('cacheTtl')) options.cacheTtl = SCORES_NETWORK_TTL; + if (!options.hasOwnProperty("cacheTtl")) + options.cacheTtl = SCORES_NETWORK_TTL; - return accSaberPlayerRankHistoryApiClient.getProcessed({...options, playerId, priority}); - } + return accSaberPlayerRankHistoryApiClient.getProcessed({ + ...options, + playerId, + priority, + }); + }; - const refreshCategories = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => { - log.debug(`Starting AccSaber categories refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService') + const refreshCategories = async ( + forceUpdate = false, + priority = queues.PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.debug( + `Starting AccSaber categories refreshing${ + forceUpdate ? " (forced)" : "" + }...`, + "AccSaberService", + ); try { - log.trace(`Fetching categories from DB...`, 'AccSaberService'); + log.trace(`Fetching categories from DB...`, "AccSaberService"); const dbCategories = await getCategories(); - log.trace(`DB categories fetched`, 'AccSaberService', dbCategories); + log.trace(`DB categories fetched`, "AccSaberService", dbCategories); - if (!await shouldRefresh('categories', forceUpdate)) return {changed: [], all: dbCategories}; + if (!(await shouldRefresh("categories", forceUpdate))) + return { changed: [], all: dbCategories }; - log.trace(`Fetching current categories from AccSaber...`, 'AccSaberService'); + log.trace( + `Fetching current categories from AccSaber...`, + "AccSaberService", + ); - let categories = await accSaberCategoriesApiClient.getProcessed({priority}); + let categories = await accSaberCategoriesApiClient.getProcessed({ + priority, + }); if (!categories || !categories.length) { - log.warn(`AccSaber returned empty categories list`, 'AccSaberService') + log.warn(`AccSaber returned empty categories list`, "AccSaberService"); return null; } - categories = categories.concat([{ - name: 'overall', - displayName: 'Overall', - countsTowardsOverall: null, - description: 'Overall' - }]); + categories = categories.concat([ + { + name: "overall", + displayName: "Overall", + countsTowardsOverall: null, + description: "Overall", + }, + ]); - log.trace(`Categories fetched`, 'AccSaberService', categories); + log.trace(`Categories fetched`, "AccSaberService", categories); - const dbCategoriesNames = dbCategories.map(c => c.name); - const newCategories = categories.filter(c => !dbCategories || !dbCategoriesNames.includes(c.name)); + const dbCategoriesNames = dbCategories.map((c) => c.name); + const newCategories = categories.filter( + (c) => !dbCategories || !dbCategoriesNames.includes(c.name), + ); if (newCategories && newCategories.length) - log.debug(`${newCategories.length} new categories found`, 'AccSaberService'); + log.debug( + `${newCategories.length} new categories found`, + "AccSaberService", + ); - await db.runInTransaction(['accsaber-categories', 'key-value'], async tx => { - const newCategoriesNames = categories.map(c => c.name); + await db.runInTransaction( + ["accsaber-categories", "key-value"], + async (tx) => { + const newCategoriesNames = categories.map((c) => c.name); - const accSaberCategoriesStore = tx.objectStore('accsaber-categories'); + const accSaberCategoriesStore = tx.objectStore("accsaber-categories"); - let cursor = await accSaberCategoriesStore.openCursor(); + let cursor = await accSaberCategoriesStore.openCursor(); - log.trace(`Remove old categories from DB`, 'AccSaberService'); + log.trace(`Remove old categories from DB`, "AccSaberService"); - while (cursor) { - const category = cursor.value; - if (!newCategoriesNames.includes(category.name)) await cursor.delete(); + while (cursor) { + const category = cursor.value; + if (!newCategoriesNames.includes(category.name)) + await cursor.delete(); - cursor = await cursor.continue(); - } + cursor = await cursor.continue(); + } - log.trace(`Old categories removed from DB`, 'AccSaberService'); + log.trace(`Old categories removed from DB`, "AccSaberService"); - log.trace(`Updating categories in DB...`, 'AccSaberService'); + log.trace(`Updating categories in DB...`, "AccSaberService"); - await Promise.all(categories.map(async c => accSaberCategoriesStore.put(c))); + await Promise.all( + categories.map(async (c) => accSaberCategoriesStore.put(c)), + ); - log.trace(`Categories updated`, 'AccSaberService'); + log.trace(`Categories updated`, "AccSaberService"); - log.trace(`Updating categories last update date in DB...`, 'AccSaberService'); + log.trace( + `Updating categories last update date in DB...`, + "AccSaberService", + ); - await tx.objectStore('key-value').put(new Date(), getLastUpdatedKey('categories')); + await tx + .objectStore("key-value") + .put(new Date(), getLastUpdatedKey("categories")); - log.debug(`Categories last update date updated`, 'AccSaberService'); - }); + log.debug(`Categories last update date updated`, "AccSaberService"); + }, + ); accSaberCategoriesRepository().addToCache(categories); - keyValueRepository().setCache(getLastUpdatedKey('categories'), new Date()); + keyValueRepository().setCache( + getLastUpdatedKey("categories"), + new Date(), + ); - log.debug(`Categories refreshing completed`, 'AccSaberService'); + log.debug(`Categories refreshing completed`, "AccSaberService"); - return {changed: newCategories, all: categories}; - } - catch(e) { + return { changed: newCategories, all: categories }; + } catch (e) { if (throwErrors) throw e; - log.debug(`Categories refreshing error`, 'AccSaberService', e) + log.debug(`Categories refreshing error`, "AccSaberService", e); return null; } - } + }; - const updatePlayerHistory = async player => { + const updatePlayerHistory = async (player) => { if (!player?.playerId) return; try { - log.debug(`Updating player ${player.playerId} history`, 'AccSaberService'); + log.debug( + `Updating player ${player.playerId} history`, + "AccSaberService", + ); const accSaberDate = toAccSaberMidnight(new Date()); const playerIdTimestamp = `${player.playerId}_${accSaberDate.getTime()}`; - const existingData = await accSaberPlayersHistoryRepository().get(playerIdTimestamp); + const existingData = + await accSaberPlayersHistoryRepository().get(playerIdTimestamp); const lastUpdated = dateFromString(existingData?.lastUpdated); if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) { - log.debug(`Refresh interval for player ${player.playerId} history not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'AccSaberService') + log.debug( + `Refresh interval for player ${ + player.playerId + } history not yet expired, skipping. Next refresh on ${formatDate( + addToDate(REFRESH_INTERVAL, lastUpdated), + )}`, + "AccSaberService", + ); return; } - const categories = (await getCategories())?.map(c => c.name) ?? null; + const categories = (await getCategories())?.map((c) => c.name) ?? null; if (!categories) { - log.trace(`No categories found, skip updating player ${player.playerId} history.`); + log.trace( + `No categories found, skip updating player ${player.playerId} history.`, + ); return; } let accStats = {}; for (const category of categories) { - const playerAccInfo = (await getRanking(category) ?? []).find(p => p.playerId === player.playerId); + const playerAccInfo = ((await getRanking(category)) ?? []).find( + (p) => p.playerId === player.playerId, + ); if (!playerAccInfo) continue; const { @@ -358,109 +485,175 @@ export default () => { accSaberDate, lastUpdated: new Date(), playerIdTimestamp, - categories: accStats - } + categories: accStats, + }; await accSaberPlayersHistoryRepository().set(stats); } else { - log.trace(`No Acc Saber data for player ${player.playerId}, skipping history updating.`, 'AccSaberService'); + log.trace( + `No Acc Saber data for player ${player.playerId}, skipping history updating.`, + "AccSaberService", + ); return; } - log.debug(`Player ${player.playerId} history updated`, 'AccSaberService'); + log.debug(`Player ${player.playerId} history updated`, "AccSaberService"); + } catch (e) { + log.debug( + `Player ${player.playerId} history updating error.`, + "AccSaberService", + e, + ); } - catch(e) { - log.debug(`Player ${player.playerId} history updating error.`, 'AccSaberService', e); - } - } + }; - const refreshRanking = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => { - log.debug(`Starting AccSaber ${category} ranking refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService') + const refreshRanking = async ( + category = "overall", + forceUpdate = false, + priority = queues.PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.debug( + `Starting AccSaber ${category} ranking refreshing${ + forceUpdate ? " (forced)" : "" + }...`, + "AccSaberService", + ); try { - log.trace(`Fetching ${category} ranking from DB...`, 'AccSaberService'); + log.trace(`Fetching ${category} ranking from DB...`, "AccSaberService"); const dbRanking = await getRanking(category); - log.trace(`DB ${category} ranking fetched`, 'AccSaberService', dbRanking); + log.trace(`DB ${category} ranking fetched`, "AccSaberService", dbRanking); - const rankingType = `${category}Ranking` + const rankingType = `${category}Ranking`; - if (!await shouldRefresh(rankingType, forceUpdate)) return dbRanking.sort((a, b) => a.rank - b.rank); + if (!(await shouldRefresh(rankingType, forceUpdate))) + return dbRanking.sort((a, b) => a.rank - b.rank); - log.trace(`Fetching current ${category} ranking from AccSaber...`, 'AccSaberService'); + log.trace( + `Fetching current ${category} ranking from AccSaber...`, + "AccSaberService", + ); - const ranking = await accSaberRankingApiClient.getProcessed({category, priority}); + const ranking = await accSaberRankingApiClient.getProcessed({ + category, + priority, + }); if (!ranking || !ranking.length) { - log.warn(`AccSaber returned empty ${category} ranking`, 'AccSaberService') + log.warn( + `AccSaber returned empty ${category} ranking`, + "AccSaberService", + ); return null; } - log.trace(`${capitalize(category)} ranking fetched`, 'AccSaberService', ranking); + log.trace( + `${capitalize(category)} ranking fetched`, + "AccSaberService", + ranking, + ); - log.trace(`Updating ${category} ranking...`, 'AccSaberService'); + log.trace(`Updating ${category} ranking...`, "AccSaberService"); - await db.runInTransaction(['accsaber-players', 'key-value'], async tx => { - const newPlayerIds = ranking.map(c => c.playerId); + await db.runInTransaction( + ["accsaber-players", "key-value"], + async (tx) => { + const newPlayerIds = ranking.map((c) => c.playerId); - const accSaberPlayersStore = tx.objectStore('accsaber-players'); + const accSaberPlayersStore = tx.objectStore("accsaber-players"); - let cursor = await accSaberPlayersStore.openCursor(); + let cursor = await accSaberPlayersStore.openCursor(); - log.trace(`Remove old players from DB for category ${category}`, 'AccSaberService'); + log.trace( + `Remove old players from DB for category ${category}`, + "AccSaberService", + ); - while (cursor) { - const player = cursor.value; - if (player.category === category && !newPlayerIds.includes(player.playerId)) await cursor.delete(); + while (cursor) { + const player = cursor.value; + if ( + player.category === category && + !newPlayerIds.includes(player.playerId) + ) + await cursor.delete(); - cursor = await cursor.continue(); - } + cursor = await cursor.continue(); + } - log.trace(`Old players removed from DB`, 'AccSaberService'); + log.trace(`Old players removed from DB`, "AccSaberService"); - log.trace(`Updating players in DB...`, 'AccSaberService'); + log.trace(`Updating players in DB...`, "AccSaberService"); - await Promise.all(ranking.map(async p => accSaberPlayersStore.put(p))); + await Promise.all( + ranking.map(async (p) => accSaberPlayersStore.put(p)), + ); - log.trace(`Players updated`, 'AccSaberService'); + log.trace(`Players updated`, "AccSaberService"); - log.trace(`Updating players last update date in DB...`, 'AccSaberService'); + log.trace( + `Updating players last update date in DB...`, + "AccSaberService", + ); - await tx.objectStore('key-value').put(new Date(), getLastUpdatedKey(rankingType)); + await tx + .objectStore("key-value") + .put(new Date(), getLastUpdatedKey(rankingType)); - log.debug(`Players last update date updated`, 'AccSaberService'); - }); + log.debug(`Players last update date updated`, "AccSaberService"); + }, + ); accSaberPlayersRepository().addToCache(ranking); keyValueRepository().setCache(getLastUpdatedKey(rankingType), new Date()); - log.debug(`${capitalize(category)} ranking refreshing completed`, 'AccSaberService'); + log.debug( + `${capitalize(category)} ranking refreshing completed`, + "AccSaberService", + ); return ranking.sort((a, b) => a.rank - b.rank); - } - catch (e) { + } catch (e) { if (throwErrors) throw e; - log.debug(` ${capitalize(category)} ranking refreshing error`, 'AccSaberService', e) + log.debug( + ` ${capitalize(category)} ranking refreshing error`, + "AccSaberService", + e, + ); return null; } - } + }; - const refreshAll = async (category = 'overall', forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => { - log.trace(`Starting AccSaber all data refreshing${forceUpdate ? ' (forced)' : ''}...`, 'AccSaberService') + const refreshAll = async ( + category = "overall", + forceUpdate = false, + priority = queues.PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.trace( + `Starting AccSaber all data refreshing${ + forceUpdate ? " (forced)" : "" + }...`, + "AccSaberService", + ); try { const dbCategories = await refreshCategories(); - if (!dbCategories || !dbCategories.all) throw 'Can not refresh categories'; + if (!dbCategories || !dbCategories.all) + throw "Can not refresh categories"; const allRankings = await Promise.all( - dbCategories.all.map(c => c.name).map(async category => refreshRanking(category)) - ) + dbCategories.all + .map((c) => c.name) + .map(async (category) => refreshRanking(category)), + ); - log.debug(`All data refreshing completed.`, 'AccSaberService') + log.debug(`All data refreshing completed.`, "AccSaberService"); const rankings = allRankings.reduce((cum, ranking) => { if (!ranking || !ranking.length) return cum; @@ -470,21 +663,28 @@ export default () => { return cum; }, {}); - Promise.all((await playerService.getAllActive()).map(async player => updatePlayerHistory(player))).then(_ => _); + Promise.all( + (await playerService.getAllActive()).map(async (player) => + updatePlayerHistory(player), + ), + ).then((_) => _); - return dbCategories.all.map(c => ({...c, ranking: rankings?.[c.name] ?? []})); + return dbCategories.all.map((c) => ({ + ...c, + ranking: rankings?.[c.name] ?? [], + })); } catch (e) { if (throwErrors) throw e; - log.debug(`All data refreshing error`, 'AccSaberService', e) + log.debug(`All data refreshing error`, "AccSaberService", e); return null; } - } - + }; + const destroyService = () => { service = null; - } + }; service = { isDataForPlayerAvailable, @@ -502,7 +702,7 @@ export default () => { refreshRanking, refreshAll, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/beatmaps.js b/src/services/beatmaps.js index b633095..b24c364 100644 --- a/src/services/beatmaps.js +++ b/src/services/beatmaps.js @@ -1,243 +1,316 @@ -import hashApiClient from '../network/clients/beatmaps/api-hash'; -import keyApiClient from '../network/clients/beatmaps/api-key'; -import {PRIORITY} from '../network/queues/http-queue'; -import log from '../utils/logger' -import {SsrHttpNotFoundError, SsrNetworkError} from '../network/errors' +import hashApiClient from "../network/clients/beatmaps/api-hash"; +import keyApiClient from "../network/clients/beatmaps/api-key"; +import { PRIORITY } from "../network/queues/http-queue"; +import log from "../utils/logger"; +import { SsrHttpNotFoundError, SsrNetworkError } from "../network/errors"; import songsBeatMapsRepository from "../db/repository/songs-beatmaps"; import cacheRepository from "../db/repository/cache"; -import {addToDate, dateFromString, HOUR} from '../utils/date' -import {capitalize, opt} from '../utils/js' +import { addToDate, dateFromString, HOUR } from "../utils/date"; +import { capitalize, opt } from "../utils/js"; -const BM_SUSPENSION_KEY = 'bmSuspension'; -const BM_NOT_FOUND_KEY = 'bm404'; +const BM_SUSPENSION_KEY = "bmSuspension"; +const BM_NOT_FOUND_KEY = "bm404"; const BM_NOT_FOUND_HOURS_BETWEEN_COUNTS = 1; const INVALID_NOTES_COUNT_FIXES = { - 'e738b38b594861745bfb0473c66ca5cca15072ff': [ - {type: 'Standard', diff: "ExpertPlus", notes: 942} - ] -} + e738b38b594861745bfb0473c66ca5cca15072ff: [ + { type: "Standard", diff: "ExpertPlus", notes: 942 }, + ], +}; export default () => { - const cacheSongInfo = async (songInfo, originalHash) => { - if (!songInfo) return null; + const cacheSongInfo = async (songInfo, originalHash) => { + if (!songInfo) return null; - const hash = originalHash && originalHash.length ? originalHash : songInfo.hash; + const hash = + originalHash && originalHash.length ? originalHash : songInfo.hash; - if (!hash || !songInfo.key) return null; + if (!hash || !songInfo.key) return null; - songInfo.hash = hash.toLowerCase(); - songInfo.key = songInfo.key.toLowerCase(); + songInfo.hash = hash.toLowerCase(); + songInfo.key = songInfo.key.toLowerCase(); - delete songInfo.description; + delete songInfo.description; - await songsBeatMapsRepository().set(songInfo); + await songsBeatMapsRepository().set(songInfo); - return songInfo; + return songInfo; + }; + + const isSuspended = (bsSuspension) => + !!bsSuspension && + bsSuspension.activeTo > new Date() && + bsSuspension.started > addToDate(-24 * HOUR); + const getCurrentSuspension = async () => + cacheRepository().get(BM_SUSPENSION_KEY); + const prolongSuspension = async (bsSuspension) => { + const current = new Date(); + + const suspension = isSuspended(bsSuspension) + ? bsSuspension + : { started: current, activeTo: new Date(), count: 0 }; + + suspension.activeTo = addToDate( + Math.pow(2, suspension.count) * HOUR, + suspension.activeTo, + ); + suspension.count++; + + return await cacheRepository().set(suspension, BM_SUSPENSION_KEY); + }; + + const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY); + const set404Hashes = async (hashes) => + cacheRepository().set(hashes, BM_NOT_FOUND_KEY); + const setHashNotFound = async (hash) => { + let songs404 = await get404Hashes(); + if (!songs404) songs404 = {}; + + const item = songs404[hash] + ? songs404[hash] + : { firstTry: new Date(), recentTry: null, count: 0 }; + + if ( + !item.recentTry || + addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) < + new Date() + ) { + item.recentTry = new Date(); + item.count++; + + songs404[hash] = item; + + await set404Hashes(songs404); } + }; + const isHashUnavailable = async (hash) => { + const songs404 = await get404Hashes(); + return songs404 && songs404[hash] && songs404[hash].count >= 3; + }; - const isSuspended = bsSuspension => !!bsSuspension && bsSuspension.activeTo > new Date() && bsSuspension.started > addToDate(-24 * HOUR); - const getCurrentSuspension = async () => cacheRepository().get(BM_SUSPENSION_KEY); - const prolongSuspension = async bsSuspension => { - const current = new Date(); + const fixInvalidNotesCount = (hash, songInfo) => { + if (!hash) return songInfo; - const suspension = isSuspended(bsSuspension) ? bsSuspension : {started: current, activeTo: new Date(), count: 0}; + if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions) + songInfo.versions.forEach((si) => { + if (!si?.diffs) return; - suspension.activeTo = addToDate(Math.pow(2, suspension.count) * HOUR, suspension.activeTo); - suspension.count++; + si.diffs.forEach((d) => { + const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find( + (f) => f.type === d?.characteristic && f.diff === d?.difficulty, + ); + if (!newNotesCnt) return; - return await cacheRepository().set(suspension, BM_SUSPENSION_KEY); - } + d.notes = newNotesCnt.notes; + }); + }); - const get404Hashes = async () => cacheRepository().get(BM_NOT_FOUND_KEY); - const set404Hashes = async hashes => cacheRepository().set(hashes, BM_NOT_FOUND_KEY); - const setHashNotFound = async hash => { - let songs404 = await get404Hashes(); - if (!songs404) songs404 = {}; + return songInfo; + }; - const item = songs404[hash] ? songs404[hash] : {firstTry: new Date(), recentTry: null, count: 0}; + const fetchSong = async ( + songInfo, + fetchFunc, + forceUpdate = false, + cacheOnly = false, + errSongId = "", + hash = null, + ) => { + if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo); - if (!item.recentTry || addToDate(BM_NOT_FOUND_HOURS_BETWEEN_COUNTS * HOUR, item.recentTry) < new Date()) { - item.recentTry = new Date(); - item.count++; + if (cacheOnly) return null; - songs404[hash] = item; + let bsSuspension = await getCurrentSuspension(); - await set404Hashes(songs404); - } - } - const isHashUnavailable = async hash => { - const songs404 = await get404Hashes(); - return songs404 && songs404[hash] && songs404[hash].count >= 3; - } + try { + if ( + isSuspended(bsSuspension) || + (hash && (await isHashUnavailable(hash))) + ) + return null; - const fixInvalidNotesCount = (hash, songInfo) => { - if (!hash) return songInfo; + const songInfo = await fetchFunc(); + if (!songInfo) { + log.warn(`Song "${errSongId}" is no longer available at BeatSaver.`); + return null; + } - if (INVALID_NOTES_COUNT_FIXES[hash] && songInfo?.versions) - songInfo.versions.forEach(si => { - if (!si?.diffs) return; - - si.diffs.forEach(d => { - const newNotesCnt = INVALID_NOTES_COUNT_FIXES[hash].find(f => f.type === d?.characteristic && f.diff === d?.difficulty); - if (!newNotesCnt) return; - - d.notes = newNotesCnt.notes; - }) - }) - - return songInfo; - } - - const fetchSong = async (songInfo, fetchFunc, forceUpdate = false, cacheOnly = false, errSongId = '', hash = null) => { - if (!forceUpdate && songInfo) return fixInvalidNotesCount(hash, songInfo); - - if(cacheOnly) return null; - - let bsSuspension = await getCurrentSuspension(); + return fixInvalidNotesCount(hash, cacheSongInfo(songInfo, hash)); + } catch (err) { + if (hash && err instanceof SsrHttpNotFoundError) { + await setHashNotFound(hash); + } + if (err instanceof SsrNetworkError && err.message === "Network error") { try { - if (isSuspended(bsSuspension) || (hash && await isHashUnavailable(hash))) return null; + await prolongSuspension(bsSuspension); + } catch {} + } - const songInfo = await fetchFunc(); - if (!songInfo) { - log.warn(`Song "${errSongId}" is no longer available at BeatSaver.`); - return null; - } + log.warn(`Error fetching BeatSaver song "${errSongId}"`); - return fixInvalidNotesCount(hash, cacheSongInfo(songInfo, hash)); - } catch (err) { - if (hash && err instanceof SsrHttpNotFoundError) { - await setHashNotFound(hash); - } - - if (err instanceof SsrNetworkError && err.message === 'Network error') { - try {await prolongSuspension(bsSuspension)} catch {} - } - - log.warn(`Error fetching BeatSaver song "${errSongId}"`); - - return null; - } + return null; } + }; - const byHash = async (hash, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => { - hash = hash.toLowerCase(); + const byHash = async ( + hash, + forceUpdate = false, + cacheOnly = false, + signal = null, + priority = PRIORITY.FG_LOW, + ) => { + hash = hash.toLowerCase(); - const songInfo = await songsBeatMapsRepository().get(hash); + const songInfo = await songsBeatMapsRepository().get(hash); - return fetchSong(songInfo, () => hashApiClient.getProcessed({hash, signal, priority}), forceUpdate, cacheOnly, hash, hash) - } + return fetchSong( + songInfo, + () => hashApiClient.getProcessed({ hash, signal, priority }), + forceUpdate, + cacheOnly, + hash, + hash, + ); + }; - const byKey = async (key, forceUpdate = false, cacheOnly = false, signal = null, priority = PRIORITY.FG_LOW) => { - key = key.toLowerCase(); + const byKey = async ( + key, + forceUpdate = false, + cacheOnly = false, + signal = null, + priority = PRIORITY.FG_LOW, + ) => { + key = key.toLowerCase(); - const songInfo = await songsBeatMapsRepository().getFromIndex('songs-beatmaps-key', key); + const songInfo = await songsBeatMapsRepository().getFromIndex( + "songs-beatmaps-key", + key, + ); - return fetchSong(songInfo, () => keyApiClient.getProcessed({key, signal, priority}), forceUpdate, cacheOnly, key) - } + return fetchSong( + songInfo, + () => keyApiClient.getProcessed({ key, signal, priority }), + forceUpdate, + cacheOnly, + key, + ); + }; - const convertOldBeatSaverToBeatMaps = song => { - let {key, hash, name, metadata: {characteristics}} = song; + const convertOldBeatSaverToBeatMaps = (song) => { + let { + key, + hash, + name, + metadata: { characteristics }, + } = song; - if (!key || !hash || !name || !characteristics || !Array.isArray(characteristics)) return null; + if ( + !key || + !hash || + !name || + !characteristics || + !Array.isArray(characteristics) + ) + return null; - if (hash.toLowerCase) hash = hash.toLowerCase(); + if (hash.toLowerCase) hash = hash.toLowerCase(); - const diffs = characteristics.reduce((diffs, ch) => { - if (!ch.name || !ch.difficulties) return diffs; - const characteristic = ch.name; + const diffs = characteristics.reduce((diffs, ch) => { + if (!ch.name || !ch.difficulties) return diffs; + const characteristic = ch.name; - return diffs.concat( - Object.entries(ch.difficulties) - .map(([difficulty, obj]) => { - if (!obj) return null; - difficulty = capitalize(difficulty); + return diffs + .concat( + Object.entries(ch.difficulties).map(([difficulty, obj]) => { + if (!obj) return null; + difficulty = capitalize(difficulty); - const seconds = opt(obj, 'length', null); - const notes = opt(obj, 'notes', null) + const seconds = opt(obj, "length", null); + const notes = opt(obj, "notes", null); - const nps = notes && seconds ? notes / seconds : null; + const nps = notes && seconds ? notes / seconds : null; - return { - njs: opt(obj, 'njs', null), - offset: opt(obj, 'njsOffset', null), - notes, - bombs: opt(obj, 'bombs', null), - obstacles: opt(obj, 'obstacles', null), - nps, - length: opt(obj, 'duration', null), - characteristic, - difficulty, - events: null, - chroma: null, - me: null, - ne: null, - cinema: null, - seconds, - paritySummary: { - errors: null, - warns: null, - resets: null, - }, - stars: null, - }; - })) - .filter(diff => diff) - }, []); - - return { - lastUpdated: dateFromString(opt(song, 'uploaded', new Date())), - oldBeatSaverId: opt(song, '_id', null), - id: key, - hash, - key, - name, - description: '', - uploader: { - id: null, - name: opt(song, 'uploader.username', null), - hash: null, - avatar: null - }, - metadata: { - bpm: opt(song, 'metadata.bpm', null), - duration: opt(song, 'metadata.duration', null), - songName: opt(song, 'metadata.songName', ''), - songSubName: opt(song, 'metadata.songSubName', ''), - songAuthorName: opt(song, 'metadata.songAuthorName', ''), - levelAuthorName: opt(song, 'metadata.levelAuthorName', '') - }, - stats: { - plays: opt(song, 'stats.plays', 0), - downloads: opt(song, 'stats.downloads', 0), - upvotes: opt(song, 'stats.upVotes', 0), - downvotes: opt(song, 'stats.downVotes', 0), - score: null - }, - uploaded: opt(song, 'uploaded', null), - automapper: !!opt(song, 'metadata.automapper', false), - ranked: null, - qualified: null, - versions: [ - { - hash, - key, - state: "Published", - createdAt: opt(song, 'uploaded', null), - sageScore: null, - diffs, - downloadURL: `https://cdn.beatsaver.com/${hash}.zip`, - coverURL: `https://cdn.beatsaver.com/${hash}.jpg`, - previewURL: `https://cdn.beatsaver.com/${hash}.mp3` - } - ] - } - } + return { + njs: opt(obj, "njs", null), + offset: opt(obj, "njsOffset", null), + notes, + bombs: opt(obj, "bombs", null), + obstacles: opt(obj, "obstacles", null), + nps, + length: opt(obj, "duration", null), + characteristic, + difficulty, + events: null, + chroma: null, + me: null, + ne: null, + cinema: null, + seconds, + paritySummary: { + errors: null, + warns: null, + resets: null, + }, + stars: null, + }; + }), + ) + .filter((diff) => diff); + }, []); return { - byHash, - byKey, - convertOldBeatSaverToBeatMaps - } -} \ No newline at end of file + lastUpdated: dateFromString(opt(song, "uploaded", new Date())), + oldBeatSaverId: opt(song, "_id", null), + id: key, + hash, + key, + name, + description: "", + uploader: { + id: null, + name: opt(song, "uploader.username", null), + hash: null, + avatar: null, + }, + metadata: { + bpm: opt(song, "metadata.bpm", null), + duration: opt(song, "metadata.duration", null), + songName: opt(song, "metadata.songName", ""), + songSubName: opt(song, "metadata.songSubName", ""), + songAuthorName: opt(song, "metadata.songAuthorName", ""), + levelAuthorName: opt(song, "metadata.levelAuthorName", ""), + }, + stats: { + plays: opt(song, "stats.plays", 0), + downloads: opt(song, "stats.downloads", 0), + upvotes: opt(song, "stats.upVotes", 0), + downvotes: opt(song, "stats.downVotes", 0), + score: null, + }, + uploaded: opt(song, "uploaded", null), + automapper: !!opt(song, "metadata.automapper", false), + ranked: null, + qualified: null, + versions: [ + { + hash, + key, + state: "Published", + createdAt: opt(song, "uploaded", null), + sageScore: null, + diffs, + downloadURL: `https://cdn.beatsaver.com/${hash}.zip`, + coverURL: `https://cdn.beatsaver.com/${hash}.jpg`, + previewURL: `https://cdn.beatsaver.com/${hash}.mp3`, + }, + ], + }; + }; + + return { + byHash, + byKey, + convertOldBeatSaverToBeatMaps, + }; +}; diff --git a/src/services/beatsavior.js b/src/services/beatsavior.js index ab6a63b..268c5bf 100644 --- a/src/services/beatsavior.js +++ b/src/services/beatsavior.js @@ -1,16 +1,24 @@ -import {PRIORITY} from '../network/queues/http-queue'; -import createPlayerService from './scoresaber/player' -import createScoresService from './scoresaber/scores' -import beatSaviorApiClient from '../network/clients/beatsavior/api'; -import beatSaviorRepository from '../db/repository/beat-savior' -import beatSaviorPlayersRepository from '../db/repository/beat-savior-players' -import {addToDate, DAY, formatDate, HOUR, MINUTE, SECOND, truncateDate} from '../utils/date' -import log from '../utils/logger' -import {opt} from '../utils/js' -import makePendingPromisePool from '../utils/pending-promises' -import {PLAYER_SCORES_PER_PAGE} from '../utils/scoresaber/consts' -import {roundToPrecision} from '../utils/format' -import {serviceFilterFunc} from './utils' +import { PRIORITY } from "../network/queues/http-queue"; +import createPlayerService from "./scoresaber/player"; +import createScoresService from "./scoresaber/scores"; +import beatSaviorApiClient from "../network/clients/beatsavior/api"; +import beatSaviorRepository from "../db/repository/beat-savior"; +import beatSaviorPlayersRepository from "../db/repository/beat-savior-players"; +import { + addToDate, + DAY, + formatDate, + HOUR, + MINUTE, + SECOND, + truncateDate, +} from "../utils/date"; +import log from "../utils/logger"; +import { opt } from "../utils/js"; +import makePendingPromisePool from "../utils/pending-promises"; +import { PLAYER_SCORES_PER_PAGE } from "../utils/scoresaber/consts"; +import { roundToPrecision } from "../utils/format"; +import { serviceFilterFunc } from "./utils"; const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 15; const CACHED_PLAYER_REFRESH_INTERVAL = HOUR * 3; @@ -30,52 +38,73 @@ export default () => { const playerService = createPlayerService(); const scoresService = createScoresService(); - const getPlayerScores = async playerId => resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, () => beatSaviorRepository().getAllFromIndex('beat-savior-playerId', playerId)); + const getPlayerScores = async (playerId) => + resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, () => + beatSaviorRepository().getAllFromIndex("beat-savior-playerId", playerId), + ); - const getPlayerScoresWithScoreSaber = async playerId => { + const getPlayerScoresWithScoreSaber = async (playerId) => { const [beatSaviorData, playerScores] = await Promise.all([ getPlayerScores(playerId), - resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () => scoresService.getPlayerScoresAsObject( - playerId, - score => score?.leaderboard?.song?.hash?.toLowerCase() ?? null, - true, - )), + resolvePromiseOrWaitForPending(`getSsPlayerScores/${playerId}`, () => + scoresService.getPlayerScoresAsObject( + playerId, + (score) => score?.leaderboard?.song?.hash?.toLowerCase() ?? null, + true, + ), + ), ]); - return beatSaviorData.map(bsData => { - if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()]) return bsData; + return beatSaviorData.map((bsData) => { + if (!bsData?.hash || !playerScores?.[bsData?.hash?.toLowerCase()]) + return bsData; - const ssScore = playerScores[bsData.hash.toLowerCase()].find(ssScore => isScoreMatchingBsData(ssScore, bsData, true)) ?? null; + const ssScore = + playerScores[bsData.hash.toLowerCase()].find((ssScore) => + isScoreMatchingBsData(ssScore, bsData, true), + ) ?? null; return { ...bsData, - ssScore - } + ssScore, + }; }); - } + }; const isScoreMatchingBsData = (score, bsData, exact = true) => { - if (!bsData.hash || !bsData.score || !bsData.timeSet || !opt(bsData, 'stats.won')) return false; + if ( + !bsData.hash || + !bsData.score || + !bsData.timeSet || + !opt(bsData, "stats.won") + ) + return false; - const diff = opt(score, 'leaderboard.diffInfo.diff'); - const scoreValue = opt(score, 'score.score'); - const timeSet = opt(score, 'score.timeSet') - let hash = opt(score, 'leaderboard.song.hash'); + const diff = opt(score, "leaderboard.diffInfo.diff"); + const scoreValue = opt(score, "score.score"); + const timeSet = opt(score, "score.timeSet"); + let hash = opt(score, "leaderboard.song.hash"); if (!diff || !score || !timeSet || !hash) return false; hash = hash.toLowerCase(); if (bsData.hash === hash && bsData.diff === diff) { - return !exact || (bsData.score === scoreValue && Math.abs(timeSet.getTime() - bsData.timeSet.getTime()) < MINUTE); + return ( + !exact || + (bsData.score === scoreValue && + Math.abs(timeSet.getTime() - bsData.timeSet.getTime()) < MINUTE) + ); } return false; - } + }; - const getScoresHistogramDefinition = (serviceParams = {sort: 'recent', order: 'desc'}) => { - const sort = serviceParams?.sort ?? 'recent'; - const order = serviceParams?.order ?? 'desc'; + const getScoresHistogramDefinition = ( + serviceParams = { sort: "recent", order: "desc" }, + ) => { + const sort = serviceParams?.sort ?? "recent"; + const order = serviceParams?.order ?? "desc"; let round = 2; let bucketSize = 1; @@ -83,57 +112,65 @@ export default () => { let maxBucketSize = null; let bucketSizeStep = null; let bucketSizeValues = null; - let type = 'linear'; - let valFunc = s => s; + let type = "linear"; + let valFunc = (s) => s; let filterFunc = serviceFilterFunc(serviceParams); - let histogramFilterFunc = s => s; - let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear' - ? roundToPrecision(valFunc(s), precision) - : truncateDate(valFunc(s), precision); - let prefix = ''; - let prefixLong = ''; - let suffix = ''; - let suffixLong = ''; + let histogramFilterFunc = (s) => s; + let roundedValFunc = (s, type = type, precision = bucketSize) => + type === "linear" + ? roundToPrecision(valFunc(s), precision) + : truncateDate(valFunc(s), precision); + let prefix = ""; + let prefixLong = ""; + let suffix = ""; + let suffixLong = ""; - switch(sort) { - case 'recent': - valFunc = s => s?.timeSet; - type = 'time'; - bucketSize = 'day' + switch (sort) { + case "recent": + valFunc = (s) => s?.timeSet; + type = "time"; + bucketSize = "day"; break; - case 'acc': - valFunc = s => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100; - histogramFilterFunc = h => h?.x >= HISTOGRAM_ACC_THRESHOLD; - type = 'linear'; + case "acc": + valFunc = (s) => (s?.trackers?.scoreTracker?.rawRatio ?? 0) * 100; + histogramFilterFunc = (h) => h?.x >= HISTOGRAM_ACC_THRESHOLD; + type = "linear"; bucketSize = 0.25; minBucketSize = 0.05; maxBucketSize = 10; bucketSizeStep = 0.05; round = 2; - suffix = '%'; - suffixLong = '%'; + suffix = "%"; + suffixLong = "%"; break; - case 'mistakes': - valFunc = s => (s?.stats?.miss ?? 0) + (s?.stats?.wallHit ?? 0) + (s?.stats?.bombHit ?? 0); - histogramFilterFunc = h => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD; - type = 'linear'; + case "mistakes": + valFunc = (s) => + (s?.stats?.miss ?? 0) + + (s?.stats?.wallHit ?? 0) + + (s?.stats?.bombHit ?? 0); + histogramFilterFunc = (h) => h?.x <= HISTOGRAM_MISTAKES_THRESHOLD; + type = "linear"; bucketSize = 1; minBucketSize = 1; maxBucketSize = 50; bucketSizeStep = 1; round = 0; - suffixLong = ' mistake(s)'; + suffixLong = " mistake(s)"; break; } return { getValue: valFunc, - getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize), + getRoundedValue: + (bucketSize = bucketSize) => + (s) => + roundedValFunc(s, type, bucketSize), filter: filterFunc, histogramFilter: histogramFilterFunc, - sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a), + sort: (a, b) => + order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a), type, bucketSize, minBucketSize, @@ -145,37 +182,42 @@ export default () => { prefixLong, suffix, suffixLong, - order - } - } + order, + }; + }; - const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => { + const getPlayerScoresPage = async ( + playerId, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + ) => { let page = serviceParams?.page ?? 1; if (page < 1) page = 1; let playerScores = await getPlayerScores(playerId); - if (!playerScores || !playerScores.length) return {total: 0, scores: []}; + if (!playerScores || !playerScores.length) return { total: 0, scores: [] }; - const {sort: sortFunc, filter: filterFunc} = getScoresHistogramDefinition(serviceParams); + const { sort: sortFunc, filter: filterFunc } = + getScoresHistogramDefinition(serviceParams); - playerScores = playerScores.filter(filterFunc).sort(sortFunc) + playerScores = playerScores.filter(filterFunc).sort(sortFunc); const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE; - if (playerScores.length < startIdx + 1) return {total: 0, scores: []}; + if (playerScores.length < startIdx + 1) return { total: 0, scores: [] }; return { total: playerScores.length, scores: playerScores .slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE) - .map(bs => { + .map((bs) => { const leaderboard = bs.leaderboard; - if (!leaderboard.leaderboardId) leaderboard.leaderboardId = bs.beatSaviorId; + if (!leaderboard.leaderboardId) + leaderboard.leaderboardId = bs.beatSaviorId; leaderboard.leaderboardId += Math.random(); // ScoresSvelte needs different keys for each scores row - const rawScore = opt(bs, 'trackers.scoreTracker.rawScore', 0); - const rawRatio = opt(bs, 'trackers.scoreTracker.rawRatio', 0); + const rawScore = opt(bs, "trackers.scoreTracker.rawScore", 0); + const rawRatio = opt(bs, "trackers.scoreTracker.rawRatio", 0); const maxScore = rawRatio & rawScore ? rawScore / rawRatio : 0; return { @@ -188,51 +230,76 @@ export default () => { score: { acc: rawRatio * 100, maxScore, - mods: opt(bs, 'trackers.scoreTracker.modifiers', null), - percentage: opt(bs, 'trackers.scoreTracker.rawRatio', 0) * 100, + mods: opt(bs, "trackers.scoreTracker.modifiers", null), + percentage: opt(bs, "trackers.scoreTracker.rawRatio", 0) * 100, pp: 0, ppWeighted: 0, rank: null, - score: opt(bs, 'trackers.scoreTracker.score', 0), + score: opt(bs, "trackers.scoreTracker.score", 0), scoreId: bs.beatSaviorId, timeSet: bs.timeSet, unmodifiedScore: rawScore, weight: 0, }, timeSet: bs.timeSet, - } - }) + }; + }), }; - } + }; const updateData = async (playerId, data) => { - log.debug(`Updating Beat Savior data for player "${playerId}"...`, 'BeatSaviorService') + log.debug( + `Updating Beat Savior data for player "${playerId}"...`, + "BeatSaviorService", + ); - await Promise.all(data.map(async d => beatSaviorRepository().set(d))); + await Promise.all(data.map(async (d) => beatSaviorRepository().set(d))); - log.debug(`Update player "${playerId}" Beat Savior last refresh date...`, 'BeatSaviorService') + log.debug( + `Update player "${playerId}" Beat Savior last refresh date...`, + "BeatSaviorService", + ); - await beatSaviorPlayersRepository().set({playerId, lastRefresh: new Date()}) + await beatSaviorPlayersRepository().set({ + playerId, + lastRefresh: new Date(), + }); - log.debug(`Beat Savior data for player "${playerId}" updated.`, 'BeatSaviorService') + log.debug( + `Beat Savior data for player "${playerId}" updated.`, + "BeatSaviorService", + ); return data; - } + }; const fetchPlayer = async (playerId, priority = PRIORITY.BG_NORMAL) => { try { - log.debug(`Fetching Beat Savior data for player "${playerId}"...`, 'BeatSaviorService'); + log.debug( + `Fetching Beat Savior data for player "${playerId}"...`, + "BeatSaviorService", + ); - const data = await beatSaviorApiClient.getProcessed({playerId, priority}); + const data = await beatSaviorApiClient.getProcessed({ + playerId, + priority, + }); if (!data) { - log.debug(`No Beat Savior data for player "${playerId}"`, 'BeatSaviorService') + log.debug( + `No Beat Savior data for player "${playerId}"`, + "BeatSaviorService", + ); return null; } // TODO: check if data already exists in DB - log.trace(`Beat Savior data for player "${playerId}" fetched`, 'BeatSaviorService', data); + log.trace( + `Beat Savior data for player "${playerId}" fetched`, + "BeatSaviorService", + data, + ); return updateData(playerId, data); } catch (err) { @@ -240,62 +307,121 @@ export default () => { return null; } - } + }; - const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => { - log.trace(`Starting refreshing BeatSavior for player "${playerId}" ${force ? ' (forced)' : ''}...`, 'BeatSaviorService') + const refresh = async ( + playerId, + force = false, + priority = PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.trace( + `Starting refreshing BeatSavior for player "${playerId}" ${ + force ? " (forced)" : "" + }...`, + "BeatSaviorService", + ); try { const player = await playerService.get(playerId); - const REFRESH_INTERVAL = playerService.isMainPlayer(playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : (player ? CACHED_PLAYER_REFRESH_INTERVAL : OTHER_PLAYER_REFRESH_INTERVAL); + const REFRESH_INTERVAL = playerService.isMainPlayer(playerId) + ? MAIN_PLAYER_REFRESH_INTERVAL + : player + ? CACHED_PLAYER_REFRESH_INTERVAL + : OTHER_PLAYER_REFRESH_INTERVAL; const bsPlayerInfo = await beatSaviorPlayersRepository().get(playerId); - const nextUpdate = bsPlayerInfo && bsPlayerInfo.lastRefresh ? addToDate(REFRESH_INTERVAL, bsPlayerInfo.lastRefresh) : addToDate(-SECOND); + const nextUpdate = + bsPlayerInfo && bsPlayerInfo.lastRefresh + ? addToDate(REFRESH_INTERVAL, bsPlayerInfo.lastRefresh) + : addToDate(-SECOND); if (!force && bsPlayerInfo && nextUpdate > new Date()) { - log.debug(`Beat Savior data is still fresh, skipping. Next refresh on ${formatDate(nextUpdate)}`, 'BeatSaviorService') + log.debug( + `Beat Savior data is still fresh, skipping. Next refresh on ${formatDate( + nextUpdate, + )}`, + "BeatSaviorService", + ); return null; if (player) { - log.trace(`Player "${playerId}" is a cached one, checking recent play date`, 'BeatSaviorService') + log.trace( + `Player "${playerId}" is a cached one, checking recent play date`, + "BeatSaviorService", + ); - if (player.recentPlay && player.recentPlay < bsPlayerInfo.lastRefresh) { - log.debug(`Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`, 'BeatSaviorService') + if ( + player.recentPlay && + player.recentPlay < bsPlayerInfo.lastRefresh + ) { + log.debug( + `Beat Savior data for player "${playerId}" was refreshed after recent play, skipping`, + "BeatSaviorService", + ); return null; } } } - return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () => fetchPlayer(playerId, priority)); + return resolvePromiseOrWaitForPending(`refresh/${playerId}`, () => + fetchPlayer(playerId, priority), + ); } catch (e) { if (throwErrors) throw e; - log.debug(`Beat Savior data refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'BeatSaviorService', e) + log.debug( + `Beat Savior data refreshing error${ + e.toString ? `: ${e.toString()}` : "" + }`, + "BeatSaviorService", + e, + ); return null; } - } + }; - const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => { - log.trace(`Starting refreshing Beat Savior data for all players${force ? ' (forced)' : ''}...`, 'BeatSaviorService'); + const refreshAll = async ( + force = false, + priority = PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.trace( + `Starting refreshing Beat Savior data for all players${ + force ? " (forced)" : "" + }...`, + "BeatSaviorService", + ); const allPlayers = await playerService.getAll(); if (!allPlayers || !allPlayers.length) { - log.trace(`No players in DB, skipping.`, 'BeatSaviorService'); + log.trace(`No players in DB, skipping.`, "BeatSaviorService"); return null; } - const allRefreshed = await Promise.all(allPlayers.map(async player => ({ - playerId: player.playerId, - beatSavior: await refresh(player.playerId, force, priority, throwErrors), - }))); + const allRefreshed = await Promise.all( + allPlayers.map(async (player) => ({ + playerId: player.playerId, + beatSavior: await refresh( + player.playerId, + force, + priority, + throwErrors, + ), + })), + ); - log.trace(`Beat Savior data for all players refreshed.`, 'BeatSaviorService', allRefreshed); + log.trace( + `Beat Savior data for all players refreshed.`, + "BeatSaviorService", + allRefreshed, + ); return allRefreshed; - } + }; const get = async (playerId, score) => { if (score && score.beatSavior) return score.beatSavior; @@ -303,12 +429,18 @@ export default () => { const playerBsData = await getPlayerScores(playerId); if (!playerBsData || !playerBsData.length) return null; - const bsData = playerBsData.find(bsData => isScoreMatchingBsData(score, bsData, true)); + const bsData = playerBsData.find((bsData) => + isScoreMatchingBsData(score, bsData, true), + ); return bsData ? bsData : null; - } + }; - const isDataForPlayerAvailable = async playerId => await beatSaviorRepository().getFromIndex('beat-savior-playerId', playerId) !== undefined; + const isDataForPlayerAvailable = async (playerId) => + (await beatSaviorRepository().getFromIndex( + "beat-savior-playerId", + playerId, + )) !== undefined; const destroyService = () => { serviceCreationCount--; @@ -319,7 +451,7 @@ export default () => { service = null; } - } + }; service = { fetchPlayer, @@ -332,7 +464,7 @@ export default () => { isDataForPlayerAvailable, getScoresHistogramDefinition, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/config.js b/src/services/config.js index 08015b0..f2d0144 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -1,7 +1,7 @@ -import keyValueRepository from '../db/repository/key-value'; -import {opt} from '../utils/js' +import keyValueRepository from "../db/repository/key-value"; +import { opt } from "../utils/js"; -const STORE_CONFIG_KEY = 'config'; +const STORE_CONFIG_KEY = "config"; let service = null; @@ -9,22 +9,23 @@ export default () => { if (service) return service; const get = async () => keyValueRepository().get(STORE_CONFIG_KEY); - const set = async config => keyValueRepository().set(config, STORE_CONFIG_KEY); + const set = async (config) => + keyValueRepository().set(config, STORE_CONFIG_KEY); const getMainPlayerId = async () => { const config = await get(); - return opt(config, 'users.main'); - } + return opt(config, "users.main"); + }; - const destroyService = () => {} + const destroyService = () => {}; service = { get, set, getMainPlayerId, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/scoresaber/leaderboard.js b/src/services/scoresaber/leaderboard.js index 4f3eda0..8d2be98 100644 --- a/src/services/scoresaber/leaderboard.js +++ b/src/services/scoresaber/leaderboard.js @@ -1,14 +1,14 @@ -import leaderboardPageClient from '../../network/clients/scoresaber/leaderboard/page-leaderboard' -import accSaberLeaderboardApiClient from '../../network/clients/accsaber/api-leaderboard' -import makePendingPromisePool from '../../utils/pending-promises' -import createPlayersService from '../../services/scoresaber/player' -import createScoresService from '../../services/scoresaber/scores' -import {PRIORITY} from '../../network/queues/http-queue' -import {LEADERBOARD_SCORES_PER_PAGE} from '../../utils/scoresaber/consts' -import {LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE} from '../../utils/accsaber/consts' -import {formatDateRelative, MINUTE} from '../../utils/date' -import {convertArrayToObjectByKey, opt} from '../../utils/js' -import eventBus from '../../utils/broadcast-channel-pubsub' +import leaderboardPageClient from "../../network/clients/scoresaber/leaderboard/page-leaderboard"; +import accSaberLeaderboardApiClient from "../../network/clients/accsaber/api-leaderboard"; +import makePendingPromisePool from "../../utils/pending-promises"; +import createPlayersService from "../../services/scoresaber/player"; +import createScoresService from "../../services/scoresaber/scores"; +import { PRIORITY } from "../../network/queues/http-queue"; +import { LEADERBOARD_SCORES_PER_PAGE } from "../../utils/scoresaber/consts"; +import { LEADERBOARD_SCORES_PER_PAGE as ACCSABER_LEADERBOARD_SCORES_PER_PAGE } from "../../utils/accsaber/consts"; +import { formatDateRelative, MINUTE } from "../../utils/date"; +import { convertArrayToObjectByKey, opt } from "../../utils/js"; +import eventBus from "../../utils/broadcast-channel-pubsub"; const ACCSABER_LEADERBOARD_NETWORK_TTL = MINUTE * 5; @@ -20,79 +20,118 @@ export default () => { const scoresService = createScoresService(); let friendsPromise = Promise.resolve([]); - const refreshFriends = async () => friendsPromise = playersService.getAll(); - eventBus.on('player-profile-removed', playerId => refreshFriends()); - eventBus.on('player-profile-added', player => refreshFriends()); - eventBus.on('player-profile-changed', player => refreshFriends()); - refreshFriends().then(_ => {}); + const refreshFriends = async () => (friendsPromise = playersService.getAll()); + eventBus.on("player-profile-removed", (playerId) => refreshFriends()); + eventBus.on("player-profile-added", (player) => refreshFriends()); + eventBus.on("player-profile-changed", (player) => refreshFriends()); + refreshFriends().then((_) => {}); const resolvePromiseOrWaitForPending = makePendingPromisePool(); - const fetchPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => resolvePromiseOrWaitForPending( - `pageClient/leaderboard/${leaderboardId}/${page}`, - () => leaderboardPageClient.getProcessed({ - leaderboardId, - page, - signal, - priority, - cacheTtl: MINUTE, - })); + const fetchPage = async ( + leaderboardId, + page = 1, + priority = PRIORITY.FG_LOW, + signal = null, + force = false, + ) => + resolvePromiseOrWaitForPending( + `pageClient/leaderboard/${leaderboardId}/${page}`, + () => + leaderboardPageClient.getProcessed({ + leaderboardId, + page, + signal, + priority, + cacheTtl: MINUTE, + }), + ); - const fetchAccSaberPage = async (leaderboardId, page = 1, priority = PRIORITY.FG_LOW, signal = null, force = false) => { + const fetchAccSaberPage = async ( + leaderboardId, + page = 1, + priority = PRIORITY.FG_LOW, + signal = null, + force = false, + ) => { if (page < 1) page = 1; const data = await resolvePromiseOrWaitForPending( `accSaberApiClient/leaderboard/${leaderboardId}/${page}`, - () => accSaberLeaderboardApiClient.getProcessed({ - leaderboardId, - page, - signal, - priority, - cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL, - })); + () => + accSaberLeaderboardApiClient.getProcessed({ + leaderboardId, + page, + signal, + priority, + cacheTtl: ACCSABER_LEADERBOARD_NETWORK_TTL, + }), + ); - if (!data || !data.scores) return data + if (!data || !data.scores) return data; const startIdx = (page - 1) * ACCSABER_LEADERBOARD_SCORES_PER_PAGE; if (data.scores.length < startIdx + 1) return data; return { ...data, - scores: data.scores - .slice(startIdx, startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE) - } - } + scores: data.scores.slice( + startIdx, + startIdx + ACCSABER_LEADERBOARD_SCORES_PER_PAGE, + ), + }; + }; - const getFriendsLeaderboard = async (leaderboardId, priority = PRIORITY.FG_LOW, signal = null) => { - const leaderboard = await resolvePromiseOrWaitForPending(`pageClient/leaderboard/${leaderboardId}/1`, () => leaderboardPageClient.getProcessed({leaderboardId, page: 1, signal, priority, cacheTtl: MINUTE})); + const getFriendsLeaderboard = async ( + leaderboardId, + priority = PRIORITY.FG_LOW, + signal = null, + ) => { + const leaderboard = await resolvePromiseOrWaitForPending( + `pageClient/leaderboard/${leaderboardId}/1`, + () => + leaderboardPageClient.getProcessed({ + leaderboardId, + page: 1, + signal, + priority, + cacheTtl: MINUTE, + }), + ); - const friends = convertArrayToObjectByKey(await friendsPromise, 'playerId'); + const friends = convertArrayToObjectByKey(await friendsPromise, "playerId"); const scores = (await scoresService.getLeaderboardScores(leaderboardId)) - .map(score => { + .map((score) => { if (!score || !score.playerId || !friends[score.playerId]) return null; const player = friends[score.playerId]; return { - player: {playerId: player.playerId, name: player.name, playerInfo: {...player.playerInfo}}, - score: {...score.score}, - } + player: { + playerId: player.playerId, + name: player.name, + playerInfo: { ...player.playerInfo }, + }, + score: { ...score.score }, + }; }) - .filter(s => s) - .sort((a, b) => opt(b, 'score.score', 0) - opt(a, 'score.score', 0)) + .filter((s) => s) + .sort((a, b) => opt(b, "score.score", 0) - opt(a, "score.score", 0)) .map((score, idx) => ({ player: score.player, - score: {...score.score, rank: idx + 1, timeSetString: formatDateRelative(score.score.timeSet)}, - })) - ; - - return {...leaderboard, scores, pageQty: 1, totalItems: scores.length}; - } + score: { + ...score.score, + rank: idx + 1, + timeSetString: formatDateRelative(score.score.timeSet), + }, + })); + return { ...leaderboard, scores, pageQty: 1, totalItems: scores.length }; + }; const destroyService = () => { service = null; - } + }; service = { fetchPage, @@ -100,7 +139,7 @@ export default () => { getFriendsLeaderboard, LEADERBOARD_SCORES_PER_PAGE, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/scoresaber/player.js b/src/services/scoresaber/player.js index 683b15b..c0e2de8 100644 --- a/src/services/scoresaber/player.js +++ b/src/services/scoresaber/player.js @@ -1,12 +1,12 @@ -import eventBus from '../../utils/broadcast-channel-pubsub' -import {configStore} from '../../stores/config' -import playerApiClient from '../../network/clients/scoresaber/player/api' -import playerFindApiClient from '../../network/clients/scoresaber/players/api-player-find' -import playerPageClient from '../../network/clients/scoresaber/player/page' -import {PRIORITY} from '../../network/queues/http-queue' -import playersRepository from '../../db/repository/players' -import playersHistoryRepository from '../../db/repository/players-history' -import log from '../../utils/logger' +import eventBus from "../../utils/broadcast-channel-pubsub"; +import { configStore } from "../../stores/config"; +import playerApiClient from "../../network/clients/scoresaber/player/api"; +import playerFindApiClient from "../../network/clients/scoresaber/players/api-player-find"; +import playerPageClient from "../../network/clients/scoresaber/player/page"; +import { PRIORITY } from "../../network/queues/http-queue"; +import playersRepository from "../../db/repository/players"; +import playersHistoryRepository from "../../db/repository/players-history"; +import log from "../../utils/logger"; import { addToDate, formatDate, @@ -14,12 +14,12 @@ import { SECOND, toSsMidnight, truncateDate, -} from '../../utils/date' -import {opt} from '../../utils/js' -import {db} from '../../db/db' -import makePendingPromisePool from '../../utils/pending-promises' -import {worker} from '../../utils/worker-wrappers' -import {getServicePlayerGain} from '../utils' +} from "../../utils/date"; +import { opt } from "../../utils/js"; +import { db } from "../../db/db"; +import makePendingPromisePool from "../../utils/pending-promises"; +import { worker } from "../../utils/worker-wrappers"; +import { getServicePlayerGain } from "../utils"; const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 3; const PLAYER_REFRESH_INTERVAL = MINUTE * 20; @@ -34,91 +34,128 @@ export default () => { const resolvePromiseOrWaitForPending = makePendingPromisePool(); - const configStoreUnsubscribe = configStore.subscribe(config => { - const newMainPlayerId = opt(config, 'users.main') + const configStoreUnsubscribe = configStore.subscribe((config) => { + const newMainPlayerId = opt(config, "users.main"); if (mainPlayerId !== newMainPlayerId) { mainPlayerId = newMainPlayerId; - log.debug(`Main player changed to ${mainPlayerId}`, 'PlayerService') + log.debug(`Main player changed to ${mainPlayerId}`, "PlayerService"); } - }) + }); - const isMainPlayer = playerId => mainPlayerId && playerId === mainPlayerId; + const isMainPlayer = (playerId) => mainPlayerId && playerId === mainPlayerId; const getAll = async (force = false) => playersRepository().getAll(force); // TODO: just for now - const getFriends = async () => (await getAll()).filter(player => player && player.playerId && !isPlayerMain(player.playerId)).map(p => p.playerId); + const getFriends = async () => + (await getAll()) + .filter( + (player) => player && player.playerId && !isPlayerMain(player.playerId), + ) + .map((p) => p.playerId); const getAllActive = async () => { const players = await getAll(); if (!players) return []; - return players.filter(player => player && player.playerInfo && !player.playerInfo.inactive && !player.playerInfo.banned); - } + return players.filter( + (player) => + player && + player.playerInfo && + !player.playerInfo.inactive && + !player.playerInfo.banned, + ); + }; - const getPlayer = async playerId => await playersRepository().get(playerId); + const getPlayer = async (playerId) => await playersRepository().get(playerId); const removePlayer = async (playerId, purgeScores = false) => { await playersRepository().delete(playerId); // TODO: purge scores if requested - eventBus.publish('player-profile-removed', playerId); - } + eventBus.publish("player-profile-removed", playerId); + }; const addPlayer = async (playerId, priority = PRIORITY.FG_LOW) => { - log.trace(`Starting to add a player "${playerId}"...`, 'PlayerService'); + log.trace(`Starting to add a player "${playerId}"...`, "PlayerService"); const player = await refresh(playerId, true, priority, false, true); if (!player) { - log.warn(`Can not add player "${playerId}"`, 'PlayerService'); + log.warn(`Can not add player "${playerId}"`, "PlayerService"); return null; } - eventBus.publish('player-profile-added', player); - eventBus.publish('player-profile-changed', player); + eventBus.publish("player-profile-added", player); + eventBus.publish("player-profile-changed", player); - log.trace(`Player "${playerId}" added.`, 'PlayerService') + log.trace(`Player "${playerId}" added.`, "PlayerService"); return player; - } + }; const setPlayer = async (player) => { await playersRepository().set(player); - eventBus.publish('player-profile-changed', player); + eventBus.publish("player-profile-changed", player); return player; - } + }; - const updatePlayer = async (player, waitForSaving = true, forceAdd = false) => { + const updatePlayer = async ( + player, + waitForSaving = true, + forceAdd = false, + ) => { if (!player || !player.playerId) { - log.warn(`Can not update player, empty playerId`, 'PlayerService', player) + log.warn( + `Can not update player, empty playerId`, + "PlayerService", + player, + ); } const dbPlayer = await getPlayer(player.playerId); if (!dbPlayer && !forceAdd) return player; - const finalPlayer = {...dbPlayer, ...player} + const finalPlayer = { ...dbPlayer, ...player }; if (!waitForSaving) { - setPlayer(finalPlayer).then(_ => _) + setPlayer(finalPlayer).then((_) => _); return finalPlayer; } return await setPlayer(finalPlayer); - } + }; - const getPlayerHistory = async playerId => resolvePromiseOrWaitForPending(`playerHistory/${playerId}`, () => playersHistoryRepository().getAllFromIndex('players-history-playerId', playerId)) + const getPlayerHistory = async (playerId) => + resolvePromiseOrWaitForPending(`playerHistory/${playerId}`, () => + playersHistoryRepository().getAllFromIndex( + "players-history-playerId", + playerId, + ), + ); - const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => getServicePlayerGain(playerHistory, toSsMidnight, 'ssDate', daysAgo, maxDaysAgo); + const getPlayerGain = (playerHistory, daysAgo = 1, maxDaysAgo = 7) => + getServicePlayerGain( + playerHistory, + toSsMidnight, + "ssDate", + daysAgo, + maxDaysAgo, + ); - const updatePlayerHistory = async player => { + const updatePlayerHistory = async (player) => { if (!player) return null; - const {playerId, profileLastUpdated, playerInfo: {banned, countries, inactive, pp, rank}, scoreStats} = player; + const { + playerId, + profileLastUpdated, + playerInfo: { banned, countries, inactive, pp, rank }, + scoreStats, + } = player; if (!playerId) return null; @@ -130,65 +167,87 @@ export default () => { const playerIdLocalTimestamp = `${playerId}_${localDate.getTime()}`; const playerIdSsTimestamp = `${playerId}_${ssDate.getTime()}`; - return playersHistoryRepository().getFromIndex('players-history-playerIdSsTimestamp', playerIdSsTimestamp) - .then(async ph => { + return playersHistoryRepository() + .getFromIndex("players-history-playerIdSsTimestamp", playerIdSsTimestamp) + .then(async (ph) => { if (ph && ph._idbId) { await playersHistoryRepository().delete(ph._idbId); - const {_idbId, ...previous} = ph; + const { _idbId, ...previous } = ph; return previous; } return null; }) - .then(async previous => { + .then(async (previous) => { let accStats = {}; if (worker) { const stats = await worker.calcPlayerStats(playerId); - const ppBoundary = await worker.calcPpBoundary(playerId) ?? null; + const ppBoundary = (await worker.calcPpBoundary(playerId)) ?? null; - const {badges, totalScore, playCount, ...playerStats} = stats ?? {}; + const { badges, totalScore, playCount, ...playerStats } = stats ?? {}; - accStats = {...playerStats} + accStats = { ...playerStats }; if (ppBoundary) accStats.ppBoundary = ppBoundary; - if (badges?.length) accStats.accBadges = badges.reduce((cum, b) => ({...cum, [b.label]: b.value}), {}); + if (badges?.length) + accStats.accBadges = badges.reduce( + (cum, b) => ({ ...cum, [b.label]: b.value }), + {}, + ); } return playersHistoryRepository().set({ ...previous, ...accStats, - playerId, banned, countries, inactive, pp, rank, ...scoreStats, - localDate, ssDate, + playerId, + banned, + countries, + inactive, + pp, + rank, + ...scoreStats, + localDate, + ssDate, playerIdLocalTimestamp, playerIdSsTimestamp, - }) + }); }) - .catch(err => {}) // swallow error - } + .catch((err) => {}); // swallow error + }; - const isPlayerMain = playerId => playerId === mainPlayerId; + const isPlayerMain = (playerId) => playerId === mainPlayerId; const getProfileFreshnessDate = (player, refreshInterval = null) => { - const lastUpdated = player && player.profileLastUpdated ? player.profileLastUpdated : null; + const lastUpdated = + player && player.profileLastUpdated ? player.profileLastUpdated : null; if (!lastUpdated) return addToDate(-SECOND); - const REFRESH_INTERVAL = refreshInterval ? refreshInterval : (isPlayerMain(player.playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : PLAYER_REFRESH_INTERVAL); + const REFRESH_INTERVAL = refreshInterval + ? refreshInterval + : isPlayerMain(player.playerId) + ? MAIN_PLAYER_REFRESH_INTERVAL + : PLAYER_REFRESH_INTERVAL; return addToDate(REFRESH_INTERVAL, lastUpdated); - } + }; - const isProfileFresh = (player, refreshInterval = null) => getProfileFreshnessDate(player, refreshInterval) > new Date(); + const isProfileFresh = (player, refreshInterval = null) => + getProfileFreshnessDate(player, refreshInterval) > new Date(); - const updatePlayerRecentPlay = async (playerId, recentPlay, recentPlayLastUpdated = new Date()) => { + const updatePlayerRecentPlay = async ( + playerId, + recentPlay, + recentPlayLastUpdated = new Date(), + ) => { let player; try { - await db.runInTransaction(['players'], async tx => { - const playersStore = tx.objectStore('players') + await db.runInTransaction(["players"], async (tx) => { + const playersStore = tx.objectStore("players"); player = await playersStore.get(playerId); if (player) { player.recentPlayLastUpdated = recentPlayLastUpdated; @@ -200,61 +259,135 @@ export default () => { if (player) { playersRepository().addToCache([player]); - eventBus.publish('player-profile-changed', player); + eventBus.publish("player-profile-changed", player); - eventBus.publish('player-recent-play-updated', {playerId, player, recentPlay, recentPlayLastUpdated}); + eventBus.publish("player-recent-play-updated", { + playerId, + player, + recentPlay, + recentPlayLastUpdated, + }); } - } - catch(err) { - // swallow error - } - } - - const fetchPlayerAndUpdateRecentPlay = async playerId => { - try { - const player = await resolvePromiseOrWaitForPending(`pageClient/${playerId}`, () =>playerPageClient.getProcessed({playerId})); - const recentPlay = opt(player, 'playerInfo.recentPlay'); - const recentPlayLastUpdated = opt(player, 'playerInfo.recentPlayLastUpdated'); - if (!recentPlay || !recentPlayLastUpdated) return null; - - return updatePlayerRecentPlay(playerId, recentPlay, recentPlayLastUpdated); } catch (err) { // swallow error } - } + }; - const isResponseCached = response => playerApiClient.isResponseCached(response); - const getDataFromResponse = response => playerApiClient.getDataFromResponse(response); + const fetchPlayerAndUpdateRecentPlay = async (playerId) => { + try { + const player = await resolvePromiseOrWaitForPending( + `pageClient/${playerId}`, + () => playerPageClient.getProcessed({ playerId }), + ); + const recentPlay = opt(player, "playerInfo.recentPlay"); + const recentPlayLastUpdated = opt( + player, + "playerInfo.recentPlayLastUpdated", + ); + if (!recentPlay || !recentPlayLastUpdated) return null; - const fetchPlayer = async (playerId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/${playerId}/${fullResponse}`, () => playerApiClient.getProcessed({...options, playerId, priority, fullResponse})); + return updatePlayerRecentPlay( + playerId, + recentPlay, + recentPlayLastUpdated, + ); + } catch (err) { + // swallow error + } + }; - const findPlayer = async (query, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => resolvePromiseOrWaitForPending(`apiClient/find/${query}/${fullResponse}`, () => playerFindApiClient.getProcessed({...options, query, priority, fullResponse})); + const isResponseCached = (response) => + playerApiClient.isResponseCached(response); + const getDataFromResponse = (response) => + playerApiClient.getDataFromResponse(response); - const fetchPlayerOrGetFromCache = async (playerId, refreshInterval = MINUTE, priority = PRIORITY.FG_LOW, signal = null, force = false) => { + const fetchPlayer = async ( + playerId, + priority = PRIORITY.FG_LOW, + { fullResponse = false, ...options } = {}, + ) => + resolvePromiseOrWaitForPending( + `apiClient/${playerId}/${fullResponse}`, + () => + playerApiClient.getProcessed({ + ...options, + playerId, + priority, + fullResponse, + }), + ); + + const findPlayer = async ( + query, + priority = PRIORITY.FG_LOW, + { fullResponse = false, ...options } = {}, + ) => + resolvePromiseOrWaitForPending( + `apiClient/find/${query}/${fullResponse}`, + () => + playerFindApiClient.getProcessed({ + ...options, + query, + priority, + fullResponse, + }), + ); + + const fetchPlayerOrGetFromCache = async ( + playerId, + refreshInterval = MINUTE, + priority = PRIORITY.FG_LOW, + signal = null, + force = false, + ) => { const player = await getPlayer(playerId); if (!player || !isProfileFresh(player, refreshInterval)) { - const fetchedPlayerResponse = await fetchPlayer(playerId, priority, {signal, cacheTtl: MINUTE, maxAge: force ? 0 : refreshInterval, fullResponse: true}); - if (isResponseCached(fetchedPlayerResponse)) return getDataFromResponse(fetchedPlayerResponse); + const fetchedPlayerResponse = await fetchPlayer(playerId, priority, { + signal, + cacheTtl: MINUTE, + maxAge: force ? 0 : refreshInterval, + fullResponse: true, + }); + if (isResponseCached(fetchedPlayerResponse)) + return getDataFromResponse(fetchedPlayerResponse); - return updatePlayer({...player, ...getDataFromResponse(fetchedPlayerResponse), profileLastUpdated: new Date()}, false) - .then(player => { - fetchPlayerAndUpdateRecentPlay(player.playerId); + return updatePlayer( + { + ...player, + ...getDataFromResponse(fetchedPlayerResponse), + profileLastUpdated: new Date(), + }, + false, + ).then((player) => { + fetchPlayerAndUpdateRecentPlay(player.playerId); - updatePlayerHistory(player); + updatePlayerHistory(player); - return player; - }) + return player; + }); } return player; - } + }; - const refresh = async (playerId, force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false, addIfNotExists = false) => { - log.trace(`Starting refreshing player "${playerId}" ${force ? ' (forced)' : ''}...`, 'PlayerService') + const refresh = async ( + playerId, + force = false, + priority = PRIORITY.BG_NORMAL, + throwErrors = false, + addIfNotExists = false, + ) => { + log.trace( + `Starting refreshing player "${playerId}" ${force ? " (forced)" : ""}...`, + "PlayerService", + ); if (!playerId) { - log.warn(`Can not refresh player if an empty playerId is given`, 'PlayerService'); + log.warn( + `Can not refresh player if an empty playerId is given`, + "PlayerService", + ); return null; } @@ -262,66 +395,101 @@ export default () => { try { let player = await getPlayer(playerId); if (!player && !addIfNotExists) { - log.debug(`Profile is not added to DB, skipping.`, 'PlayerService') + log.debug(`Profile is not added to DB, skipping.`, "PlayerService"); return null; } - log.trace(`Player fetched from DB`, 'PlayerService', player); + log.trace(`Player fetched from DB`, "PlayerService", player); if (!force) { const profileFreshnessDate = getProfileFreshnessDate(player); if (profileFreshnessDate > new Date()) { - - log.debug(`Profile is still fresh, skipping. Next refresh on ${formatDate(profileFreshnessDate)}`, 'PlayerService') + log.debug( + `Profile is still fresh, skipping. Next refresh on ${formatDate( + profileFreshnessDate, + )}`, + "PlayerService", + ); return player; } } - log.trace(`Fetching player ${playerId} from ScoreSaber...`, 'PlayerService') + log.trace( + `Fetching player ${playerId} from ScoreSaber...`, + "PlayerService", + ); const fetchedPlayer = await fetchPlayer(playerId, priority); - if (!fetchedPlayer || !fetchedPlayer.playerId || !fetchedPlayer.name || !fetchedPlayer.playerInfo || !fetchedPlayer.scoreStats) { - log.warn(`ScoreSaber returned empty info for player ${playerId}`, 'PlayerService') + if ( + !fetchedPlayer || + !fetchedPlayer.playerId || + !fetchedPlayer.name || + !fetchedPlayer.playerInfo || + !fetchedPlayer.scoreStats + ) { + log.warn( + `ScoreSaber returned empty info for player ${playerId}`, + "PlayerService", + ); return null; } - log.trace(`Player fetched`, 'PlayerService', fetchedPlayer); + log.trace(`Player fetched`, "PlayerService", fetchedPlayer); - player = await updatePlayer({...fetchedPlayer, profileLastUpdated: new Date()}, true, addIfNotExists); + player = await updatePlayer( + { ...fetchedPlayer, profileLastUpdated: new Date() }, + true, + addIfNotExists, + ); - updatePlayerHistory(player).then(_ => _); + updatePlayerHistory(player).then((_) => _); - log.debug(`Player refreshed.`, 'PlayerService', player); + log.debug(`Player refreshed.`, "PlayerService", player); return player; } catch (e) { if (throwErrors) throw e; - log.debug(`Player refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'PlayerService', e) + log.debug( + `Player refreshing error${e.toString ? `: ${e.toString()}` : ""}`, + "PlayerService", + e, + ); return null; } - } + }; - const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => { - log.trace(`Starting refreshing all players${force ? ' (forced)' : ''}...`, 'PlayerService'); + const refreshAll = async ( + force = false, + priority = PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.trace( + `Starting refreshing all players${force ? " (forced)" : ""}...`, + "PlayerService", + ); const allPlayers = await getAll(); if (!allPlayers || !allPlayers.length) { - log.trace(`No players in DB, skipping.`, 'PlayerService'); + log.trace(`No players in DB, skipping.`, "PlayerService"); return null; } - const allRefreshed = await Promise.all(allPlayers.map(player => refresh(player.playerId, force, priority, throwErrors))); + const allRefreshed = await Promise.all( + allPlayers.map((player) => + refresh(player.playerId, force, priority, throwErrors), + ), + ); - log.trace(`All players refreshed.`, 'PlayerService', allRefreshed); + log.trace(`All players refreshed.`, "PlayerService", allRefreshed); return allRefreshed; - } + }; const destroyService = () => { serviceCreationCount--; @@ -331,7 +499,7 @@ export default () => { service = null; } - } + }; service = { isMainPlayer, @@ -356,7 +524,7 @@ export default () => { destroyService, isResponseCached, getDataFromResponse, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/scoresaber/pp.js b/src/services/scoresaber/pp.js index 05686e0..d1349a7 100644 --- a/src/services/scoresaber/pp.js +++ b/src/services/scoresaber/pp.js @@ -1,6 +1,6 @@ -import createScoresService from './scores' -import makePendingPromisePool from '../../utils/pending-promises' -import {getTotalPpFromSortedPps} from '../../utils/scoresaber/pp' +import createScoresService from "./scores"; +import makePendingPromisePool from "../../utils/pending-promises"; +import { getTotalPpFromSortedPps } from "../../utils/scoresaber/pp"; let service = null; let serviceCreationCount = 0; @@ -12,28 +12,32 @@ export default () => { const resolvePromiseOrWaitForPending = makePendingPromisePool(); - const getTotalPp = scores => scores && Array.isArray(scores) - ? getTotalPpFromSortedPps( - scores - .filter(s => s.pp > 0) - .map(s => s.pp) - .sort((a, b) => b - a), - ) - : null; + const getTotalPp = (scores) => + scores && Array.isArray(scores) + ? getTotalPpFromSortedPps( + scores + .filter((s) => s.pp > 0) + .map((s) => s.pp) + .sort((a, b) => b - a), + ) + : null; - const getTotalPlayerPp = async (playerId, modifiedScores = {}) => getTotalPp( - Object.values({ - ...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () => scoresService.getPlayerScoresAsObject(playerId))), - ...modifiedScores, - }), - ); + const getTotalPlayerPp = async (playerId, modifiedScores = {}) => + getTotalPp( + Object.values({ + ...(await resolvePromiseOrWaitForPending(`scores/${playerId}`, () => + scoresService.getPlayerScoresAsObject(playerId), + )), + ...modifiedScores, + }), + ); async function getWhatIfScore(playerId, leaderboardId, pp = 0) { const currentTotalPp = await getTotalPlayerPp(playerId); if (!currentTotalPp) return null; const newTotalPp = await getTotalPlayerPp(playerId, { - [leaderboardId]: {pp}, + [leaderboardId]: { pp }, }); return { @@ -67,7 +71,7 @@ export default () => { if (!acc || acc <= 0) { return 0; } - let index = ppCurve.findIndex(o => o.at >= acc); + let index = ppCurve.findIndex((o) => o.at >= acc); if (index === -1) { return ppCurve[ppCurve.length - 1].value; } @@ -83,7 +87,7 @@ export default () => { function accFromPpFactor(ppFactor) { if (!ppFactor || ppFactor <= 0) return 0; - const idx = ppCurve.findIndex(o => o.value >= ppFactor); + const idx = ppCurve.findIndex((o) => o.value >= ppFactor); if (idx < 0) return ppCurve[ppCurve.length - 1].at; const from = ppCurve[idx - 1]; @@ -101,7 +105,7 @@ export default () => { service = null; } - } + }; service = { getWhatIfScore, @@ -111,7 +115,7 @@ export default () => { accFromPpFactor, PP_PER_STAR, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/scoresaber/rankeds.js b/src/services/scoresaber/rankeds.js index 71bbf39..aeee8fe 100644 --- a/src/services/scoresaber/rankeds.js +++ b/src/services/scoresaber/rankeds.js @@ -1,13 +1,17 @@ -import {db} from '../../db/db' -import queues from '../../network/queues/queues'; -import rankedsPageClient from '../../network/clients/scoresaber/rankeds/page'; -import eventBus from '../../utils/broadcast-channel-pubsub' -import {arrayDifference, convertArrayToObjectByKey, opt} from '../../utils/js' -import rankedsRepository from '../../db/repository/rankeds' -import rankedsChangesRepository from '../../db/repository/rankeds-changes' -import keyValueRepository from '../../db/repository/key-value' -import log from '../../utils/logger' -import {addToDate, formatDate, HOUR} from '../../utils/date' +import { db } from "../../db/db"; +import queues from "../../network/queues/queues"; +import rankedsPageClient from "../../network/clients/scoresaber/rankeds/page"; +import eventBus from "../../utils/broadcast-channel-pubsub"; +import { + arrayDifference, + convertArrayToObjectByKey, + opt, +} from "../../utils/js"; +import rankedsRepository from "../../db/repository/rankeds"; +import rankedsChangesRepository from "../../db/repository/rankeds-changes"; +import keyValueRepository from "../../db/repository/key-value"; +import log from "../../utils/logger"; +import { addToDate, formatDate, HOUR } from "../../utils/date"; const REFRESH_INTERVAL = HOUR; @@ -16,16 +20,27 @@ export default () => { if (service) return service; const getRankeds = async () => { - const dbRankeds = await rankedsRepository().getAll() + const dbRankeds = await rankedsRepository().getAll(); - return dbRankeds ? convertArrayToObjectByKey(dbRankeds, 'leaderboardId') : {} - } + return dbRankeds + ? convertArrayToObjectByKey(dbRankeds, "leaderboardId") + : {}; + }; - const getLastUpdated = async () => keyValueRepository().get('rankedsLastUpdated'); - const setLastUpdated = async date => keyValueRepository().set(date, 'rankedsLastUpdated'); + const getLastUpdated = async () => + keyValueRepository().get("rankedsLastUpdated"); + const setLastUpdated = async (date) => + keyValueRepository().set(date, "rankedsLastUpdated"); - const refreshRankeds = async (forceUpdate = false, priority = queues.PRIORITY.BG_NORMAL, throwErrors = false) => { - log.trace(`Starting rankeds refreshing${forceUpdate ? ' (forced)' : ''}...`, 'RankedsService') + const refreshRankeds = async ( + forceUpdate = false, + priority = queues.PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.trace( + `Starting rankeds refreshing${forceUpdate ? " (forced)" : ""}...`, + "RankedsService", + ); try { let fetchedRankedSongs; @@ -33,40 +48,50 @@ export default () => { if (!forceUpdate) { const lastUpdated = await getLastUpdated(); if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) { - log.debug(`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'RankedsService') + log.debug( + `Refresh interval not yet expired, skipping. Next refresh on ${formatDate( + addToDate(REFRESH_INTERVAL, lastUpdated), + )}`, + "RankedsService", + ); return null; } } - log.trace(`Fetching current rankeds from ScoreSaber...`, 'RankedsService') - fetchedRankedSongs = await rankedsPageClient.getProcessed({priority}); + log.trace( + `Fetching current rankeds from ScoreSaber...`, + "RankedsService", + ); + fetchedRankedSongs = await rankedsPageClient.getProcessed({ priority }); if (!fetchedRankedSongs || !fetchedRankedSongs.length) { - log.warn(`ScoreSaber returned empty rankeds list`, 'RankedsService') + log.warn(`ScoreSaber returned empty rankeds list`, "RankedsService"); return null; } - log.trace('Fetching rankeds from DB', 'RankedsService'); + log.trace("Fetching rankeds from DB", "RankedsService"); const oldRankedSongs = await getRankeds(); // add firstSeen & oldStars properties fetchedRankedSongs = convertArrayToObjectByKey( - fetchedRankedSongs.map(s => { - const firstSeen = oldRankedSongs[s.leaderboardId] && oldRankedSongs[s.leaderboardId].firstSeen - ? oldRankedSongs[s.leaderboardId].firstSeen - : new Date(); + fetchedRankedSongs.map((s) => { + const firstSeen = + oldRankedSongs[s.leaderboardId] && + oldRankedSongs[s.leaderboardId].firstSeen + ? oldRankedSongs[s.leaderboardId].firstSeen + : new Date(); - return {...s, firstSeen, oldStars: null} + return { ...s, firstSeen, oldStars: null }; }), - 'leaderboardId', + "leaderboardId", ); // find differences between old and new ranked songs const newRankeds = arrayDifference( Object.keys(fetchedRankedSongs), Object.keys(oldRankedSongs), - ).map(leaderboardId => ({ + ).map((leaderboardId) => ({ leaderboardId: parseInt(leaderboardId, 10), oldStars: null, stars: fetchedRankedSongs[leaderboardId].stars, @@ -74,77 +99,99 @@ export default () => { })); if (newRankeds && newRankeds.length) - log.debug(`${newRankeds.length} ranked(s) found`, 'RankedsService'); + log.debug(`${newRankeds.length} ranked(s) found`, "RankedsService"); const changed = // concat new rankeds with changed rankeds - newRankeds - .concat( - Object.values(oldRankedSongs) - .filter(s => s.stars !== (fetchedRankedSongs[s.leaderboardId] ? opt(fetchedRankedSongs[s.leaderboardId], 'stars', null) : null)) - .map(s => ({ - leaderboardId: s.leaderboardId, - oldStars: s.stars, - stars: opt(fetchedRankedSongs[s.leaderboardId], 'stars', null), - timestamp: Date.now(), - }), - ) - ); + newRankeds.concat( + Object.values(oldRankedSongs) + .filter( + (s) => + s.stars !== + (fetchedRankedSongs[s.leaderboardId] + ? opt(fetchedRankedSongs[s.leaderboardId], "stars", null) + : null), + ) + .map((s) => ({ + leaderboardId: s.leaderboardId, + oldStars: s.stars, + stars: opt(fetchedRankedSongs[s.leaderboardId], "stars", null), + timestamp: Date.now(), + })), + ); - if(newRankeds && changed && changed.length - newRankeds.length > 0) - log.debug(`${changed.length - newRankeds.length} changed ranked(s) found`, 'RankedsService'); + if (newRankeds && changed && changed.length - newRankeds.length > 0) + log.debug( + `${changed.length - newRankeds.length} changed ranked(s) found`, + "RankedsService", + ); const changedLeaderboards = changed - .map(s => { - const ranked = fetchedRankedSongs[s.leaderboardId] ? fetchedRankedSongs[s.leaderboardId] : oldRankedSongs[s.leaderboardId]; + .map((s) => { + const ranked = fetchedRankedSongs[s.leaderboardId] + ? fetchedRankedSongs[s.leaderboardId] + : oldRankedSongs[s.leaderboardId]; - return { - ...ranked, - ...s, - } - }, - ) - .filter(s => s && s.hash) - .map(l => { - const {oldStars, timestamp, ...leaderboard} = l; + return { + ...ranked, + ...s, + }; + }) + .filter((s) => s && s.hash) + .map((l) => { + const { oldStars, timestamp, ...leaderboard } = l; return leaderboard; }); - log.trace('Saving rankeds to DB...', 'RankedsService'); + log.trace("Saving rankeds to DB...", "RankedsService"); - await db.runInTransaction(['rankeds', 'rankeds-changes', 'key-value'], async tx => { - await Promise.all(changedLeaderboards.map(async ranked => rankedsRepository().set(ranked, undefined, tx))); - await Promise.all(changed.map(async rc => rankedsChangesRepository().set(rc, undefined, tx))); - await setLastUpdated(new Date()) - }); + await db.runInTransaction( + ["rankeds", "rankeds-changes", "key-value"], + async (tx) => { + await Promise.all( + changedLeaderboards.map(async (ranked) => + rankedsRepository().set(ranked, undefined, tx), + ), + ); + await Promise.all( + changed.map(async (rc) => + rankedsChangesRepository().set(rc, undefined, tx), + ), + ); + await setLastUpdated(new Date()); + }, + ); - log.trace('Rankeds saved', 'RankedsService'); + log.trace("Rankeds saved", "RankedsService"); if (changed.length) { - eventBus.publish('rankeds-changed', {changed, allRankeds: fetchedRankedSongs}); + eventBus.publish("rankeds-changed", { + changed, + allRankeds: fetchedRankedSongs, + }); } - log.debug(`Rankeds refreshing complete.`, 'RankedsService') + log.debug(`Rankeds refreshing complete.`, "RankedsService"); return changed; } catch (e) { if (throwErrors) throw e; - log.debug(`Rankeds refreshing error`, 'RankedsService', e) + log.debug(`Rankeds refreshing error`, "RankedsService", e); return null; } - } + }; const destroyService = () => { service = null; - } + }; service = { get: getRankeds, refresh: refreshRankeds, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/scoresaber/ranking.js b/src/services/scoresaber/ranking.js index 28fbcc2..e88cb91 100644 --- a/src/services/scoresaber/ranking.js +++ b/src/services/scoresaber/ranking.js @@ -1,10 +1,10 @@ -import playersGlobalRankingApiClient from '../../network/clients/scoresaber/players/api-ranking-global' -import playersGlobalRankingPagesApiClient from '../../network/clients/scoresaber/players/api-ranking-global-pages' -import playersCountryRankingPageClient from '../../network/clients/scoresaber/players/page-ranking-country' -import makePendingPromisePool from '../../utils/pending-promises' -import {PRIORITY} from '../../network/queues/http-queue' -import {PLAYERS_PER_PAGE} from '../../utils/scoresaber/consts' -import {opt} from '../../utils/js' +import playersGlobalRankingApiClient from "../../network/clients/scoresaber/players/api-ranking-global"; +import playersGlobalRankingPagesApiClient from "../../network/clients/scoresaber/players/api-ranking-global-pages"; +import playersCountryRankingPageClient from "../../network/clients/scoresaber/players/page-ranking-country"; +import makePendingPromisePool from "../../utils/pending-promises"; +import { PRIORITY } from "../../network/queues/http-queue"; +import { PLAYERS_PER_PAGE } from "../../utils/scoresaber/consts"; +import { opt } from "../../utils/js"; let service = null; export default () => { @@ -12,24 +12,52 @@ export default () => { const resolvePromiseOrWaitForPending = makePendingPromisePool(); - const fetchGlobal = async (page = 1, priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`apiClient/ranking/global/${page}`, () => playersGlobalRankingApiClient.getProcessed({page, signal, priority})); + const fetchGlobal = async ( + page = 1, + priority = PRIORITY.FG_LOW, + signal = null, + ) => + resolvePromiseOrWaitForPending(`apiClient/ranking/global/${page}`, () => + playersGlobalRankingApiClient.getProcessed({ page, signal, priority }), + ); - const fetchCountry = async (country, page = 1, priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`pageClient/ranking/${country}/${page}`, () => playersCountryRankingPageClient.getProcessed({country, page, signal, priority})); + const fetchCountry = async ( + country, + page = 1, + priority = PRIORITY.FG_LOW, + signal = null, + ) => + resolvePromiseOrWaitForPending( + `pageClient/ranking/${country}/${page}`, + () => + playersCountryRankingPageClient.getProcessed({ + country, + page, + signal, + priority, + }), + ); - const fetchGlobalPages = async (priority = PRIORITY.FG_LOW, signal = null) => resolvePromiseOrWaitForPending(`apiClient/rankingGlobalPages`, () => playersGlobalRankingPagesApiClient.getProcessed({signal, priority})); + const fetchGlobalPages = async (priority = PRIORITY.FG_LOW, signal = null) => + resolvePromiseOrWaitForPending(`apiClient/rankingGlobalPages`, () => + playersGlobalRankingPagesApiClient.getProcessed({ signal, priority }), + ); - const fetchGlobalCount = async (priority = PRIORITY.FG_LOW, signal = null) => { + const fetchGlobalCount = async ( + priority = PRIORITY.FG_LOW, + signal = null, + ) => { const pages = await fetchGlobalPages(priority, signal); if (!pages || !Number.isFinite(pages)) return 0; return pages * PLAYERS_PER_PAGE; - } + }; async function fetchMiniRanking(rank, country = null, numOfPlayers = 5) { try { if (!Number.isFinite(numOfPlayers)) numOfPlayers = 5; - const getPage = rank => Math.floor((rank - 1) / PLAYERS_PER_PAGE) + 1; + const getPage = (rank) => Math.floor((rank - 1) / PLAYERS_PER_PAGE) + 1; const playerPage = getPage(rank); let firstPlayerRank = rank - (numOfPlayers - (numOfPlayers > 2 ? 2 : 1)); @@ -38,25 +66,33 @@ export default () => { const lastPlayerRank = firstPlayerRank + numOfPlayers - 1; const lastPlayerRankPage = getPage(lastPlayerRank); - const pages = [...new Set([playerPage, firstPlayerRankPage, lastPlayerRankPage])].filter(p => p); + const pages = [ + ...new Set([playerPage, firstPlayerRankPage, lastPlayerRankPage]), + ].filter((p) => p); - const ranking = (await Promise.all(pages.map(async page => (country ? fetchCountry(country, page) : fetchGlobal(page))))) + const ranking = ( + await Promise.all( + pages.map(async (page) => + country ? fetchCountry(country, page) : fetchGlobal(page), + ), + ) + ) .reduce((cum, arr) => cum.concat(arr), []) - .filter(player => { - const rank = opt(player, 'playerInfo.rank') + .filter((player) => { + const rank = opt(player, "playerInfo.rank"); return rank >= firstPlayerRank && rank <= lastPlayerRank; }) - .sort((a,b) => opt(a, 'playerInfo.rank') - opt(b, 'playerInfo.rank')) + .sort((a, b) => opt(a, "playerInfo.rank") - opt(b, "playerInfo.rank")); return ranking; - } catch(err) { + } catch (err) { return null; } } const destroyService = () => { service = null; - } + }; service = { getGlobal: fetchGlobal, @@ -66,7 +102,7 @@ export default () => { getMiniRanking: fetchMiniRanking, PLAYERS_PER_PAGE, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/scoresaber/scores.js b/src/services/scoresaber/scores.js index 750bd31..0c089f6 100644 --- a/src/services/scoresaber/scores.js +++ b/src/services/scoresaber/scores.js @@ -1,23 +1,30 @@ -import {db} from '../../db/db' -import eventBus from '../../utils/broadcast-channel-pubsub' -import {configStore} from '../../stores/config' -import createPlayerService from './player'; -import createRankedsStore from '../../stores/scoresaber/rankeds' -import {PRIORITY} from '../../network/queues/http-queue' -import recentScoresApiClient from '../../network/clients/scoresaber/scores/api-recent' -import topScoresApiClient from '../../network/clients/scoresaber/scores/api-top' -import playersRepository from '../../db/repository/players' -import scoresRepository from '../../db/repository/scores' -import scoresUpdateQueueRepository from '../../db/repository/scores-update-queue' -import log from '../../utils/logger' -import {addToDate, formatDate, HOUR, MINUTE, SECOND, truncateDate} from '../../utils/date' -import {opt} from '../../utils/js' -import scores from '../../db/repository/scores' -import {SsrHttpNotFoundError} from '../../network/errors' -import {PLAYER_SCORES_PER_PAGE} from '../../utils/scoresaber/consts' -import makePendingPromisePool from '../../utils/pending-promises' -import {roundToPrecision} from '../../utils/format' -import {serviceFilterFunc} from '../utils' +import { db } from "../../db/db"; +import eventBus from "../../utils/broadcast-channel-pubsub"; +import { configStore } from "../../stores/config"; +import createPlayerService from "./player"; +import createRankedsStore from "../../stores/scoresaber/rankeds"; +import { PRIORITY } from "../../network/queues/http-queue"; +import recentScoresApiClient from "../../network/clients/scoresaber/scores/api-recent"; +import topScoresApiClient from "../../network/clients/scoresaber/scores/api-top"; +import playersRepository from "../../db/repository/players"; +import scoresRepository from "../../db/repository/scores"; +import scoresUpdateQueueRepository from "../../db/repository/scores-update-queue"; +import log from "../../utils/logger"; +import { + addToDate, + formatDate, + HOUR, + MINUTE, + SECOND, + truncateDate, +} from "../../utils/date"; +import { opt } from "../../utils/js"; +import scores from "../../db/repository/scores"; +import { SsrHttpNotFoundError } from "../../network/errors"; +import { PLAYER_SCORES_PER_PAGE } from "../../utils/scoresaber/consts"; +import makePendingPromisePool from "../../utils/pending-promises"; +import { roundToPrecision } from "../../utils/format"; +import { serviceFilterFunc } from "../utils"; const MAIN_PLAYER_REFRESH_INTERVAL = MINUTE * 3; const PLAYER_REFRESH_INTERVAL = MINUTE * 30; @@ -44,109 +51,155 @@ export default () => { let refreshCallCounter = 0; - const refreshingFinished = async (samplingTime = 100, timeout = 30000) => new Promise((resolve, reject) => { - let callCounter = 0; - const maxCallCount = samplingTime ? timeout / samplingTime : timeout; + const refreshingFinished = async (samplingTime = 100, timeout = 30000) => + new Promise((resolve, reject) => { + let callCounter = 0; + const maxCallCount = samplingTime ? timeout / samplingTime : timeout; - const sampler = () => { - if (refreshCallCounter === 0) { - resolve(true); - return; - } + const sampler = () => { + if (refreshCallCounter === 0) { + resolve(true); + return; + } - callCounter++; - if (callCounter > maxCallCount) { - reject(timeout); - return; - } + callCounter++; + if (callCounter > maxCallCount) { + reject(timeout); + return; + } - setTimeout(sampler, samplingTime); - } + setTimeout(sampler, samplingTime); + }; - sampler(); - }) + sampler(); + }); - const configStoreUnsubscribe = configStore.subscribe(config => { - const newMainPlayerId = opt(config, 'users.main') + const configStoreUnsubscribe = configStore.subscribe((config) => { + const newMainPlayerId = opt(config, "users.main"); if (mainPlayerId !== newMainPlayerId) { mainPlayerId = newMainPlayerId; - log.debug(`Main player changed to ${mainPlayerId}`, 'ScoresService') + log.debug(`Main player changed to ${mainPlayerId}`, "ScoresService"); } - }) + }); let rankedStoreUnsubscribe = null; - createRankedsStore().then(rankedStore => { - rankedStoreUnsubscribe = rankedStore.subscribe(rankeds => { - allRankeds = rankeds + createRankedsStore().then((rankedStore) => { + rankedStoreUnsubscribe = rankedStore.subscribe((rankeds) => { + allRankeds = rankeds; - log.debug(`Ranked songs updated`, 'ScoresService', allRankeds) - }) - }) + log.debug(`Ranked songs updated`, "ScoresService", allRankeds); + }); + }); - const isDataForPlayerAvailable = async playerId => (await Promise.all([ - scoresRepository().getFromIndex('scores-playerId', playerId), - playersRepository().get(playerId), - ])) - .every(p => p !== undefined); + const isDataForPlayerAvailable = async (playerId) => + ( + await Promise.all([ + scoresRepository().getFromIndex("scores-playerId", playerId), + playersRepository().get(playerId), + ]) + ).every((p) => p !== undefined); const getAllScores = async () => scoresRepository().getAll(); - const getLeaderboardScores = async leaderboardId => scoresRepository().getAllFromIndex('scores-leaderboardId', leaderboardId); - const getPlayerScores = async playerId => resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, async () => { - return (await scoresRepository().getAllFromIndex('scores-playerId', playerId)) - .map(s => ({...s, leaderboard: {...s?.leaderboard, stars: allRankeds[s?.leaderboardId]?.stars ?? null}})) - }) - const getPlayerScoresAsObject = async (playerId, idFunc = score => opt(score, 'leaderboard.leaderboardId'), asArray = false) => convertScoresToObject(await getPlayerScores(playerId), idFunc, asArray) - const getPlayerSongScore = async (playerId, leaderboardId) => scoresRepository().get(playerId + '_' + leaderboardId); - const getPlayerRankedScores = async playerId => { - const [scores, rankeds] = await Promise.all([getPlayerScores(playerId), allRankeds]); + const getLeaderboardScores = async (leaderboardId) => + scoresRepository().getAllFromIndex("scores-leaderboardId", leaderboardId); + const getPlayerScores = async (playerId) => + resolvePromiseOrWaitForPending(`getPlayerScores/${playerId}`, async () => { + return ( + await scoresRepository().getAllFromIndex("scores-playerId", playerId) + ).map((s) => ({ + ...s, + leaderboard: { + ...s?.leaderboard, + stars: allRankeds[s?.leaderboardId]?.stars ?? null, + }, + })); + }); + const getPlayerScoresAsObject = async ( + playerId, + idFunc = (score) => opt(score, "leaderboard.leaderboardId"), + asArray = false, + ) => convertScoresToObject(await getPlayerScores(playerId), idFunc, asArray); + const getPlayerSongScore = async (playerId, leaderboardId) => + scoresRepository().get(playerId + "_" + leaderboardId); + const getPlayerRankedScores = async (playerId) => { + const [scores, rankeds] = await Promise.all([ + getPlayerScores(playerId), + allRankeds, + ]); if (!scores) return []; - return scores.filter(s => s.leaderboardId && rankeds[s.leaderboardId]); - } - const updateScore = async score => scoresRepository().set(score); + return scores.filter((s) => s.leaderboardId && rankeds[s.leaderboardId]); + }; + const updateScore = async (score) => scoresRepository().set(score); - const reduceScoresArr = scores => scores.reduce((allScores, scorePage) => [...allScores, ...scorePage], []); - const isAnyScoreOlderThan = (scores, olderThan) => scores.some(s => s.score && s.score.timeSet && s.score.timeSet <= olderThan); - const createFetchUntilLastUpdated = olderThan => scores => isAnyScoreOlderThan(scores, olderThan); + const reduceScoresArr = (scores) => + scores.reduce((allScores, scorePage) => [...allScores, ...scorePage], []); + const isAnyScoreOlderThan = (scores, olderThan) => + scores.some( + (s) => s.score && s.score.timeSet && s.score.timeSet <= olderThan, + ); + const createFetchUntilLastUpdated = (olderThan) => (scores) => + isAnyScoreOlderThan(scores, olderThan); - const convertScoresToObject = (scores, idFunc = score => opt(score, 'leaderboard.leaderboardId'), asArray = false) => scores.reduce((scoresObj, score) => { - const _id = idFunc(score); - if (!_id) return scoresObj; + const convertScoresToObject = ( + scores, + idFunc = (score) => opt(score, "leaderboard.leaderboardId"), + asArray = false, + ) => + scores.reduce((scoresObj, score) => { + const _id = idFunc(score); + if (!_id) return scoresObj; - if (asArray) { - if (!scoresObj[_id]) scoresObj[_id] = []; + if (asArray) { + if (!scoresObj[_id]) scoresObj[_id] = []; - scoresObj[_id].push({...score}) - } else { - scoresObj[_id] = {...score}; - } + scoresObj[_id].push({ ...score }); + } else { + scoresObj[_id] = { ...score }; + } - return scoresObj; - }, {}) + return scoresObj; + }, {}); const getScoreKey = (playerId, score) => { - const leaderboardId = opt(score, 'leaderboard.leaderboardId'); + const leaderboardId = opt(score, "leaderboard.leaderboardId"); if (!leaderboardId) return null; return `${playerId}_${leaderboardId}`; - } - const convertScoresById = (playerId, scores) => convertScoresToObject(scores, score => getScoreKey(playerId, score)); + }; + const convertScoresById = (playerId, scores) => + convertScoresToObject(scores, (score) => getScoreKey(playerId, score)); - const fetchScoresUntil = async (playerId, startPage = 1, priority = PRIORITY.BG_NORMAL, signal = null, untilFunc = null, dontReduce = false) => { - log.debug(`Fetching scores of player "${playerId}" starting from page #${startPage}`, 'ScoresService'); + const fetchScoresUntil = async ( + playerId, + startPage = 1, + priority = PRIORITY.BG_NORMAL, + signal = null, + untilFunc = null, + dontReduce = false, + ) => { + log.debug( + `Fetching scores of player "${playerId}" starting from page #${startPage}`, + "ScoresService", + ); let data = []; let page = startPage; while (page) { try { - log.trace(`Fetching scores page #${page}`, 'ScoresService'); + log.trace(`Fetching scores page #${page}`, "ScoresService"); - const pageData = await recentScoresApiClient.getProcessed({playerId, page, signal, priority}); - log.trace(`Scores page #${page} fetched`, 'ScoresService', pageData); + const pageData = await recentScoresApiClient.getProcessed({ + playerId, + page, + signal, + priority, + }); + log.trace(`Scores page #${page} fetched`, "ScoresService", pageData); if (!pageData) { - log.trace(`Scores page #${page} is empty`, 'ScoresService'); + log.trace(`Scores page #${page} is empty`, "ScoresService"); break; } @@ -158,7 +211,9 @@ export default () => { (untilFunc && untilFunc(pageData)) ) { // push only relevant scores and return - data.push(pageData.filter(score => !untilFunc || !untilFunc([score]))); + data.push( + pageData.filter((score) => !untilFunc || !untilFunc([score])), + ); break; } @@ -169,7 +224,7 @@ export default () => { if (!(err instanceof SsrHttpNotFoundError)) throw err; // stop fetching at 404 - log.trace(`Received 404 Not Found, abort download`, 'ScoresService'); + log.trace(`Received 404 Not Found, abort download`, "ScoresService"); break; } @@ -177,34 +232,67 @@ export default () => { } return dontReduce ? data : reduceScoresArr(data); - } + }; - const fetchAllScores = async (playerId, numOfPages, priority = PRIORITY.BG_NORMAL, signal = null) => { - log.debug(`Fetching all scores of player "${playerId}, number of pages: ${numOfPages}`, 'ScoresService'); + const fetchAllScores = async ( + playerId, + numOfPages, + priority = PRIORITY.BG_NORMAL, + signal = null, + ) => { + log.debug( + `Fetching all scores of player "${playerId}, number of pages: ${numOfPages}`, + "ScoresService", + ); - const pages = Array(numOfPages).fill(0).map((_, idx) => idx + 1); + const pages = Array(numOfPages) + .fill(0) + .map((_, idx) => idx + 1); - let data = await Promise.all(pages.map(page => recentScoresApiClient.getProcessed({playerId, page, signal, priority}))); + let data = await Promise.all( + pages.map((page) => + recentScoresApiClient.getProcessed({ + playerId, + page, + signal, + priority, + }), + ), + ); if (!data || !data.length) return []; if (data[data.length - 1].length === PLAYER_SCORES_PER_PAGE) { data = [ ...data, - ...(await fetchScoresUntil(playerId, data.length + 1, priority, signal, null, true)), + ...(await fetchScoresUntil( + playerId, + data.length + 1, + priority, + signal, + null, + true, + )), ]; } return reduceScoresArr(data); - } + }; - const getRecentPlayFromScores = (scores, defaultRecentPlay = null) => scores.reduce((recentPlay, s) => opt(s, 'score.timeSet') && s.score.timeSet > recentPlay ? s.score.timeSet : recentPlay, defaultRecentPlay); + const getRecentPlayFromScores = (scores, defaultRecentPlay = null) => + scores.reduce( + (recentPlay, s) => + opt(s, "score.timeSet") && s.score.timeSet > recentPlay + ? s.score.timeSet + : recentPlay, + defaultRecentPlay, + ); const addScoreIndexFields = (playerId, score) => { const id = getScoreKey(playerId, score); - const leaderboardId = opt(score, 'leaderboard.leaderboardId'); - const timeSet = opt(score, 'score.timeSet'); - const pp = opt(score, 'score.pp'); + const leaderboardId = opt(score, "leaderboard.leaderboardId"); + const timeSet = opt(score, "score.timeSet"); + const pp = opt(score, "score.pp"); return { ...score, @@ -212,99 +300,130 @@ export default () => { playerId, leaderboardId, timeSet, - pp - } - } + pp, + }; + }; const updateRankAndPpFromTheQueue = async () => { - log.debug('Processing rank and pp update queue', 'ScoresService'); + log.debug("Processing rank and pp update queue", "ScoresService"); try { - log.debug('Rank and pp update queue, waiting for the scores to finish refreshing.', 'ScoresService'); + log.debug( + "Rank and pp update queue, waiting for the scores to finish refreshing.", + "ScoresService", + ); await refreshingFinished(); - log.debug('Rank and pp update queue, scores refreshed, start queue processing.', 'ScoresService'); + log.debug( + "Rank and pp update queue, scores refreshed, start queue processing.", + "ScoresService", + ); - await db.runInTransaction(['scores-update-queue', 'scores'], async tx => { - // get all scores updates at least one minute old (some time to download new scores) - let cursor = await tx.objectStore('scores-update-queue').index('scores-update-queue-fetchedAt').openCursor(IDBKeyRange.upperBound(addToDate(-1 * MINUTE))); - const scoresStore = tx.objectStore('scores'); + await db.runInTransaction( + ["scores-update-queue", "scores"], + async (tx) => { + // get all scores updates at least one minute old (some time to download new scores) + let cursor = await tx + .objectStore("scores-update-queue") + .index("scores-update-queue-fetchedAt") + .openCursor(IDBKeyRange.upperBound(addToDate(-1 * MINUTE))); + const scoresStore = tx.objectStore("scores"); - while (cursor) { - try { - const scoreUpdate = {...cursor.value}; + while (cursor) { + try { + const scoreUpdate = { ...cursor.value }; - await cursor.delete(); + await cursor.delete(); + + if (!scoreUpdate.id) { + cursor = await cursor.continue(); + continue; + } + + const dbScore = await scoresStore.get(scoreUpdate.id); + if (!dbScore) { + cursor = await cursor.continue(); + continue; + } + + const scoreLastUpdated = dbScore.lastUpdated; + + if ( + (!scoreLastUpdated || + scoreLastUpdated < scoreUpdate.fetchedAt) && + (opt(dbScore, "score.scoreId") === + opt(scoreUpdate, "score.scoreId") || + opt(dbScore, "score.unmodifiedScore") <= + opt(scoreUpdate, "score.unmodifiedScore")) && + opt(scoreUpdate, "score.scoreId") && + !!opt(scoreUpdate, "score.timeSet") + ) { + dbScore.score = scoreUpdate.score; + + dbScore.pp = scoreUpdate.score.pp; + dbScore.timeSet = scoreUpdate.score.timeSet; + dbScore.lastUpdated = new Date(); + + await scoresStore.put(dbScore); + scoresRepository().addToCache([dbScore]); + } - if (!scoreUpdate.id) { cursor = await cursor.continue(); - continue; + } catch (err) { + // swallow error + if (cursor) cursor = await cursor.continue(); } - - const dbScore = await scoresStore.get(scoreUpdate.id) - if (!dbScore) { - cursor = await cursor.continue(); - continue; - } - - const scoreLastUpdated = dbScore.lastUpdated; - - if ( - (!scoreLastUpdated || scoreLastUpdated < scoreUpdate.fetchedAt) && - ( - opt(dbScore, 'score.scoreId') === opt(scoreUpdate, 'score.scoreId') || - (opt(dbScore, 'score.unmodifiedScore') <= opt(scoreUpdate, 'score.unmodifiedScore')) - ) && - opt(scoreUpdate, 'score.scoreId') && !!opt(scoreUpdate, 'score.timeSet') - ) { - dbScore.score = scoreUpdate.score; - - dbScore.pp = scoreUpdate.score.pp; - dbScore.timeSet = scoreUpdate.score.timeSet; - dbScore.lastUpdated = new Date(); - - await scoresStore.put(dbScore); - scoresRepository().addToCache([dbScore]) - } - - cursor = await cursor.continue(); - } catch (err) { - // swallow error - if (cursor) cursor = await cursor.continue(); } - } - }) + }, + ); - log.debug('Rank and pp update queue processed.', 'ScoresService'); + log.debug("Rank and pp update queue processed.", "ScoresService"); + } catch (err) { + log.debug( + "Rank and pp update queue has NOT been processed.", + "ScoresService", + ); } - catch(err) { - log.debug('Rank and pp update queue has NOT been processed.', 'ScoresService'); - } - } + }; - const addRankAndPpToUpdateQueue = async scoresToUpdate => { + const addRankAndPpToUpdateQueue = async (scoresToUpdate) => { if (!scoresToUpdate || !scoresToUpdate.length) return; - log.debug('Queueing rank and pp update for bunch of scores', 'ScoresService', scoresToUpdate); + log.debug( + "Queueing rank and pp update for bunch of scores", + "ScoresService", + scoresToUpdate, + ); try { - await Promise.all(scoresToUpdate.map(async s => scoresUpdateQueueRepository().set(s))) - } - catch(err) { + await Promise.all( + scoresToUpdate.map(async (s) => scoresUpdateQueueRepository().set(s)), + ); + } catch (err) { // swallow error } - log.debug('Scores rank & pp queued for update.', 'ScoresService', scoresToUpdate) - } + log.debug( + "Scores rank & pp queued for update.", + "ScoresService", + scoresToUpdate, + ); + }; const updatePlayerScores = async (player, priority = PRIORITY.BG_NORMAL) => { if (!player || !player.playerId) { - log.warn(`Can not refresh scores, empty playerId`, 'ScoresService', player); + log.warn( + `Can not refresh scores, empty playerId`, + "ScoresService", + player, + ); return null; } - const numOfScores = opt(player, 'scoreStats.totalPlayCount', null); - const numOfPages = numOfScores ? Math.ceil(numOfScores / PLAYER_SCORES_PER_PAGE) : null; + const numOfScores = opt(player, "scoreStats.totalPlayCount", null); + const numOfPages = numOfScores + ? Math.ceil(numOfScores / PLAYER_SCORES_PER_PAGE) + : null; const newLastUpdated = new Date(); @@ -312,8 +431,11 @@ export default () => { let newScores; const abortController = new AbortController(); - const playerScores = await getPlayerScores(player.playerId) - const currentScoresById = convertScoresById(player.playerId, playerScores); + const playerScores = await getPlayerScores(player.playerId); + const currentScoresById = convertScoresById( + player.playerId, + playerScores, + ); let mostRecentPlayFromScores = null; if (!player.recentPlay) { @@ -321,20 +443,44 @@ export default () => { player.recentPlay = mostRecentPlayFromScores; } - const startUpdatingDate = !player.scoresLastUpdated || (mostRecentPlayFromScores && mostRecentPlayFromScores < player.scoresLastUpdated) - ? mostRecentPlayFromScores - : player.scoresLastUpdated; + const startUpdatingDate = + !player.scoresLastUpdated || + (mostRecentPlayFromScores && + mostRecentPlayFromScores < player.scoresLastUpdated) + ? mostRecentPlayFromScores + : player.scoresLastUpdated; - if (numOfPages && !startUpdatingDate) newScores = await fetchAllScores(player.playerId, numOfPages, priority, abortController.signal); - else newScores = await fetchScoresUntil(player.playerId, 1, priority, abortController.signal, createFetchUntilLastUpdated(startUpdatingDate)) + if (numOfPages && !startUpdatingDate) + newScores = await fetchAllScores( + player.playerId, + numOfPages, + priority, + abortController.signal, + ); + else + newScores = await fetchScoresUntil( + player.playerId, + 1, + priority, + abortController.signal, + createFetchUntilLastUpdated(startUpdatingDate), + ); if (!newScores || !newScores.length) { // no new scores - just update player profile - const playerData = {...player, scoresLastUpdated: newLastUpdated, recentPlayLastUpdated: newLastUpdated} + const playerData = { + ...player, + scoresLastUpdated: newLastUpdated, + recentPlayLastUpdated: newLastUpdated, + }; await playersRepository().set(playerData); - return {recentPlay: player.recentPlay, newScores: null, scores: currentScoresById}; + return { + recentPlay: player.recentPlay, + newScores: null, + scores: currentScoresById, + }; } const recentPlay = getRecentPlayFromScores(newScores, player.recentPlay); @@ -342,8 +488,8 @@ export default () => { // TODO: calculate pp contribution of score let updatedScores = []; - await db.runInTransaction(['scores', 'players'], async tx => { - const playersStore = tx.objectStore('players') + await db.runInTransaction(["scores", "players"], async (tx) => { + const playersStore = tx.objectStore("players"); player = await playersStore.get(player.playerId); player.scoresLastUpdated = newLastUpdated; player.recentPlayLastUpdated = newLastUpdated; @@ -352,26 +498,41 @@ export default () => { await playersStore.put(player); playersRepository().addToCache([player]); - const scoresStore = tx.objectStore('scores'); + const scoresStore = tx.objectStore("scores"); - for(let score of newScores) { + for (let score of newScores) { const id = getScoreKey(player.playerId, score); - const leaderboardId = opt(score, 'leaderboard.leaderboardId'); - const scoreValue = opt(score, 'score.score'); - const unmodifiedScore = opt(score, 'score.unmodifiedScore') - const scoreTimeSet = opt(score, 'score.timeSet'); - const scorePp = opt(score, 'score.pp'); + const leaderboardId = opt(score, "leaderboard.leaderboardId"); + const scoreValue = opt(score, "score.score"); + const unmodifiedScore = opt(score, "score.unmodifiedScore"); + const scoreTimeSet = opt(score, "score.timeSet"); + const scorePp = opt(score, "score.pp"); - if (!id || !leaderboardId || !scoreTimeSet || scoreValue === undefined || scorePp === undefined) { + if ( + !id || + !leaderboardId || + !scoreTimeSet || + scoreValue === undefined || + scorePp === undefined + ) { return null; } - const dbScore = await scoresStore.get(id) + const dbScore = await scoresStore.get(id); if (dbScore) { - const prevScoreScorePart = {...dbScore.score}; - if (prevScoreScorePart && prevScoreScorePart.timeSet && prevScoreScorePart.score !== undefined && prevScoreScorePart.unmodifiedScore < unmodifiedScore) { - const prevHistory = opt(dbScore, 'history.length') ? dbScore.history.filter(h => h.timeSet) : []; - score.history = [prevScoreScorePart].concat(prevHistory).slice(0,3); + const prevScoreScorePart = { ...dbScore.score }; + if ( + prevScoreScorePart && + prevScoreScorePart.timeSet && + prevScoreScorePart.score !== undefined && + prevScoreScorePart.unmodifiedScore < unmodifiedScore + ) { + const prevHistory = opt(dbScore, "history.length") + ? dbScore.history.filter((h) => h.timeSet) + : []; + score.history = [prevScoreScorePart] + .concat(prevHistory) + .slice(0, 3); } } @@ -385,40 +546,63 @@ export default () => { } }); - return {player, recentPlay, newScores, scores: {...currentScoresById, ...convertScoresToObject(updatedScores, score => opt(score, 'id'))}}; + return { + player, + recentPlay, + newScores, + scores: { + ...currentScoresById, + ...convertScoresToObject(updatedScores, (score) => opt(score, "id")), + }, + }; } catch (err) { - if (![opt(err, 'name'), opt(err, 'message')].includes('AbortError')) throw err; + if (![opt(err, "name"), opt(err, "message")].includes("AbortError")) + throw err; return null; } - } + }; - const isPlayerMain = playerId => playerId === mainPlayerId; + const isPlayerMain = (playerId) => playerId === mainPlayerId; - const getScoresFreshnessDate = (player, refreshInterval = null, key = 'scoresLastUpdated') => { + const getScoresFreshnessDate = ( + player, + refreshInterval = null, + key = "scoresLastUpdated", + ) => { const lastUpdated = player && player[key] ? player[key] : null; if (!lastUpdated) return addToDate(-SECOND); - const REFRESH_INTERVAL = refreshInterval ? refreshInterval : (isPlayerMain(player.playerId) ? MAIN_PLAYER_REFRESH_INTERVAL : PLAYER_REFRESH_INTERVAL); + const REFRESH_INTERVAL = refreshInterval + ? refreshInterval + : isPlayerMain(player.playerId) + ? MAIN_PLAYER_REFRESH_INTERVAL + : PLAYER_REFRESH_INTERVAL; return addToDate(REFRESH_INTERVAL, lastUpdated); - } + }; - const isScoreDateFresh = (player, refreshInterval = null, key = 'scoresLastUpdated') => getScoresFreshnessDate(player, refreshInterval, key) > new Date(); + const isScoreDateFresh = ( + player, + refreshInterval = null, + key = "scoresLastUpdated", + ) => getScoresFreshnessDate(player, refreshInterval, key) > new Date(); - const getPlayerScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}) => { + const getPlayerScoresPage = async ( + playerId, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + ) => { let page = serviceParams?.page ?? 1; if (page < 1) page = 1; - let playerScores = (await getPlayerScores(playerId)); + let playerScores = await getPlayerScores(playerId); if (!playerScores || !playerScores.length) return null; - const {sort: sortFunc, filter: filterFunc} = getScoresHistogramDefinition(serviceParams); + const { sort: sortFunc, filter: filterFunc } = + getScoresHistogramDefinition(serviceParams); - playerScores = playerScores - .filter(filterFunc) - .sort(sortFunc); + playerScores = playerScores.filter(filterFunc).sort(sortFunc); const startIdx = (page - 1) * PLAYER_SCORES_PER_PAGE; @@ -426,13 +610,15 @@ export default () => { return { total: playerScores.length, - scores: playerScores.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE) + scores: playerScores.slice(startIdx, startIdx + PLAYER_SCORES_PER_PAGE), }; - } + }; - const getScoresHistogramDefinition = (serviceParams = {sort: 'recent', order: 'desc'}) => { - const sort = serviceParams?.sort ?? 'recent'; - const order = serviceParams?.order ?? 'desc'; + const getScoresHistogramDefinition = ( + serviceParams = { sort: "recent", order: "desc" }, + ) => { + const sort = serviceParams?.sort ?? "recent"; + const order = serviceParams?.order ?? "desc"; const commonFilterFunc = serviceFilterFunc(serviceParams); @@ -442,82 +628,91 @@ export default () => { let maxBucketSize = null; let bucketSizeStep = null; let bucketSizeValues = null; - let type = 'linear'; - let valFunc = s => s; + let type = "linear"; + let valFunc = (s) => s; let filterFunc = commonFilterFunc; - let histogramFilterFunc = h => h; - let roundedValFunc = (s, type = type, precision = bucketSize) => type === 'linear' - ? roundToPrecision(valFunc(s), precision) - : truncateDate(valFunc(s), precision); - let prefix = ''; - let prefixLong = ''; - let suffix = ''; - let suffixLong = ''; + let histogramFilterFunc = (h) => h; + let roundedValFunc = (s, type = type, precision = bucketSize) => + type === "linear" + ? roundToPrecision(valFunc(s), precision) + : truncateDate(valFunc(s), precision); + let prefix = ""; + let prefixLong = ""; + let suffix = ""; + let suffixLong = ""; - switch(sort) { - case 'recent': - valFunc = s => s?.timeSet; - type = 'time'; - bucketSize = 'day' + switch (sort) { + case "recent": + valFunc = (s) => s?.timeSet; + type = "time"; + bucketSize = "day"; break; - case 'top': - valFunc = s => s?.pp ?? 0; - filterFunc = s => (s?.pp ?? 0) > 0 && commonFilterFunc(s) - type = 'linear'; + case "top": + valFunc = (s) => s?.pp ?? 0; + filterFunc = (s) => (s?.pp ?? 0) > 0 && commonFilterFunc(s); + type = "linear"; bucketSize = HISTOGRAM_PP_PRECISION; minBucketSize = 1; maxBucketSize = 100; bucketSizeStep = 1; round = 0; - suffix = 'pp'; - suffixLong = 'pp'; + suffix = "pp"; + suffixLong = "pp"; break; - case 'rank': - valFunc = s => s?.score?.rank ?? 1000000; - type = 'linear'; + case "rank": + valFunc = (s) => s?.score?.rank ?? 1000000; + type = "linear"; bucketSize = HISTOGRAM_RANK_PRECISION; minBucketSize = 1; maxBucketSize = 100; bucketSizeStep = 1; round = 0; - prefixLong = '#'; + prefixLong = "#"; break; - case 'acc': - valFunc = s => s?.score?.maxScore && s?.score?.unmodifiedScore ? s.score.unmodifiedScore / s.score.maxScore * 100 : (s?.score?.acc ?? null); - filterFunc = s => (valFunc(s) ?? 0) > 0 && commonFilterFunc(s) - type = 'linear'; + case "acc": + valFunc = (s) => + s?.score?.maxScore && s?.score?.unmodifiedScore + ? (s.score.unmodifiedScore / s.score.maxScore) * 100 + : s?.score?.acc ?? null; + filterFunc = (s) => (valFunc(s) ?? 0) > 0 && commonFilterFunc(s); + type = "linear"; bucketSize = HISTOGRAM_ACC_PRECISION; minBucketSize = 0.05; maxBucketSize = 10; bucketSizeStep = 0.05; round = 2; - suffix = '%'; - suffixLong = '%'; + suffix = "%"; + suffixLong = "%"; break; - case 'stars': - valFunc = s => s?.leaderboard?.stars ?? null; - filterFunc = s => (s?.leaderboard?.stars ?? 0) > 0 && commonFilterFunc(s) - type = 'linear'; + case "stars": + valFunc = (s) => s?.leaderboard?.stars ?? null; + filterFunc = (s) => + (s?.leaderboard?.stars ?? 0) > 0 && commonFilterFunc(s); + type = "linear"; bucketSize = HISTOGRAM_STARS_PRECISION; minBucketSize = 0.1; maxBucketSize = 10; bucketSizeStep = 0.1; round = 2; - suffix = '★'; - suffixLong = '★'; + suffix = "★"; + suffixLong = "★"; break; } return { getValue: valFunc, - getRoundedValue: (bucketSize = bucketSize) => s => roundedValFunc(s, type, bucketSize), + getRoundedValue: + (bucketSize = bucketSize) => + (s) => + roundedValFunc(s, type, bucketSize), filter: filterFunc, histogramFilter: histogramFilterFunc, - sort: (a, b) => order === 'asc' ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a), + sort: (a, b) => + order === "asc" ? valFunc(a) - valFunc(b) : valFunc(b) - valFunc(a), type, bucketSize, minBucketSize, @@ -529,34 +724,66 @@ export default () => { prefixLong, suffix, suffixLong, - order - } - } + order, + }; + }; - const fetchScoresPage = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}, priority = PRIORITY.FG_LOW, {...options} = {}) => - ((serviceParams?.sort ?? 'recent') === 'top' ? topScoresApiClient : recentScoresApiClient) - .getProcessed({...options, playerId, page: serviceParams?.page ?? 1, priority}); + const fetchScoresPage = async ( + playerId, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + priority = PRIORITY.FG_LOW, + { ...options } = {}, + ) => + ((serviceParams?.sort ?? "recent") === "top" + ? topScoresApiClient + : recentScoresApiClient + ).getProcessed({ + ...options, + playerId, + page: serviceParams?.page ?? 1, + priority, + }); - const fetchScoresPageAndUpdateIfNeeded = async (playerId, serviceParams = {sort: 'recent', order: 'desc', page: 1}, priority = PRIORITY.FG_LOW, signal = null, canUseBrowserCache = false, refreshInterval = MINUTE) => { - const fetchedScoresResponse = await fetchScoresPage(playerId, serviceParams, priority, {signal, cacheTtl: MINUTE, maxAge: canUseBrowserCache ? 0 : refreshInterval, fullResponse: true}); - if (topScoresApiClient.isResponseCached(fetchedScoresResponse)) return topScoresApiClient.getDataFromResponse(fetchedScoresResponse); + const fetchScoresPageAndUpdateIfNeeded = async ( + playerId, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + priority = PRIORITY.FG_LOW, + signal = null, + canUseBrowserCache = false, + refreshInterval = MINUTE, + ) => { + const fetchedScoresResponse = await fetchScoresPage( + playerId, + serviceParams, + priority, + { + signal, + cacheTtl: MINUTE, + maxAge: canUseBrowserCache ? 0 : refreshInterval, + fullResponse: true, + }, + ); + if (topScoresApiClient.isResponseCached(fetchedScoresResponse)) + return topScoresApiClient.getDataFromResponse(fetchedScoresResponse); - const fetchedScores = topScoresApiClient.getDataFromResponse(fetchedScoresResponse); + const fetchedScores = topScoresApiClient.getDataFromResponse( + fetchedScoresResponse, + ); const playerScores = await getPlayerScores(playerId); if (fetchedScores && playerScores && playerScores.length) { - const playerScoresObj = convertScoresToObject(playerScores) + const playerScoresObj = convertScoresToObject(playerScores); // update rank and pp in DB const scoresToUpdate = fetchedScores - .map(score => { + .map((score) => { try { score = addScoreIndexFields(playerId, score); - const leaderboardId = opt(score, 'leaderboard.leaderboardId') + const leaderboardId = opt(score, "leaderboard.leaderboardId"); if (!leaderboardId || !score.id) return null; - const scoreObj = opt(score, 'score') + const scoreObj = opt(score, "score"); if (!scoreObj || !Object.keys(scoreObj).length) return null; const cachedScore = playerScoresObj[leaderboardId]; @@ -565,80 +792,139 @@ export default () => { const id = score.id; const lastUpdated = cachedScore.lastUpdated; - if (lastUpdated && lastUpdated > addToDate(-RANK_AND_PP_REFRESH_INTERVAL)) return null; + if ( + lastUpdated && + lastUpdated > addToDate(-RANK_AND_PP_REFRESH_INTERVAL) + ) + return null; if ( (!lastUpdated || lastUpdated < score.fetchedAt) && - ( - opt(cachedScore, 'score.scoreId') === opt(score, 'score.scoreId') || - (opt(cachedScore, 'score.unmodifiedScore') <= opt(score, 'score.unmodifiedScore')) - ) + (opt(cachedScore, "score.scoreId") === + opt(score, "score.scoreId") || + opt(cachedScore, "score.unmodifiedScore") <= + opt(score, "score.unmodifiedScore")) ) { - scoresRepository().addToCache([{...cachedScore, score: {...scoreObj}}]); + scoresRepository().addToCache([ + { ...cachedScore, score: { ...scoreObj } }, + ]); } - return {id, leaderboardId, fetchedAt: score.fetchedAt, score: {...scoreObj}} + return { + id, + leaderboardId, + fetchedAt: score.fetchedAt, + score: { ...scoreObj }, + }; } catch { return null; } }) - .filter(score => score) + .filter((score) => score); - if (scoresToUpdate.length) addRankAndPpToUpdateQueue(scoresToUpdate).then(_ => {}); + if (scoresToUpdate.length) + addRankAndPpToUpdateQueue(scoresToUpdate).then((_) => {}); } return fetchedScores; - } + }; - const fetchScoresPageOrGetFromCache = async (player, serviceParams = {sort: 'recent', order: 'desc', page: 1}, refreshInterval = MINUTE, priority = PRIORITY.FG_LOW, signal = null, force = false) => { + const fetchScoresPageOrGetFromCache = async ( + player, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + refreshInterval = MINUTE, + priority = PRIORITY.FG_LOW, + signal = null, + force = false, + ) => { if (!player || !player.playerId) return null; - const canUseBrowserCache = !force && isScoreDateFresh(player, refreshInterval, 'recentPlayLastUpdated') + const canUseBrowserCache = + !force && + isScoreDateFresh(player, refreshInterval, "recentPlayLastUpdated"); - const scoresPage = await getPlayerScoresPage(player.playerId, serviceParams); + const scoresPage = await getPlayerScoresPage( + player.playerId, + serviceParams, + ); - if - (Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val)?.length || - !['recent', 'top'].includes(serviceParams.sort ?? 'recent') - ) return scoresPage; + if ( + Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val) + ?.length || + !["recent", "top"].includes(serviceParams.sort ?? "recent") + ) + return scoresPage; - const scores = Array.isArray(scoresPage) ? scoresPage : (scoresPage?.scores ?? []); + const scores = Array.isArray(scoresPage) + ? scoresPage + : scoresPage?.scores ?? []; // force fetch from time to time even when in cache (in order to update rank/pp) OR if cached score is ranked and pp === 0 - const shouldPageBeRefetched = scores && scores.reduce((shouldRefresh, score) => { - if (!score.pp && allRankeds[score.leaderboard]) return true; + const shouldPageBeRefetched = + scores && + scores.reduce((shouldRefresh, score) => { + if (!score.pp && allRankeds[score.leaderboard]) return true; - if (!score.lastUpdated || score.lastUpdated < addToDate(-RANK_AND_PP_REFRESH_INTERVAL)) return true; + if ( + !score.lastUpdated || + score.lastUpdated < addToDate(-RANK_AND_PP_REFRESH_INTERVAL) + ) + return true; - return shouldRefresh - }, false) + return shouldRefresh; + }, false); if ( force || !scoresPage || shouldPageBeRefetched || - !isScoreDateFresh(player, refreshInterval, 'recentPlayLastUpdated') || - !player.recentPlay || !player.scoresLastUpdated || player.recentPlay > player.scoresLastUpdated + !isScoreDateFresh(player, refreshInterval, "recentPlayLastUpdated") || + !player.recentPlay || + !player.scoresLastUpdated || + player.recentPlay > player.scoresLastUpdated ) - return fetchScoresPageAndUpdateIfNeeded(player.playerId, serviceParams, priority, signal, canUseBrowserCache && !shouldPageBeRefetched, refreshInterval); + return fetchScoresPageAndUpdateIfNeeded( + player.playerId, + serviceParams, + priority, + signal, + canUseBrowserCache && !shouldPageBeRefetched, + refreshInterval, + ); return scoresPage; - } + }; - const refresh = async (playerId, forceUpdate = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => { + const refresh = async ( + playerId, + forceUpdate = false, + priority = PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { refreshCallCounter++; try { - log.trace(`Starting player "${playerId}" scores refreshing${forceUpdate ? ' (forced)' : ''}...`, 'ScoresService') + log.trace( + `Starting player "${playerId}" scores refreshing${ + forceUpdate ? " (forced)" : "" + }...`, + "ScoresService", + ); if (!playerId) { - log.warn(`Can not refresh player scores if an empty playerId is given`, 'ScoresService'); + log.warn( + `Can not refresh player scores if an empty playerId is given`, + "ScoresService", + ); return null; } if (updateInProgress.includes(playerId)) { - log.warn(`Player "${playerId}" scores are being fetched, skipping.`, 'ScoresService'); + log.warn( + `Player "${playerId}" scores are being fetched, skipping.`, + "ScoresService", + ); return null; } @@ -648,88 +934,142 @@ export default () => { let player; player = await playerService.refresh(playerId, false, priority); - if (!player) player = await playerService.refresh(playerId, true, priority); + if (!player) + player = await playerService.refresh(playerId, true, priority); if (!player) { - log.debug(`Can not refresh the scores of player "${playerId}" because it has not been added to the DB`); + log.debug( + `Can not refresh the scores of player "${playerId}" because it has not been added to the DB`, + ); return null; } if (!forceUpdate) { const scoresFreshnessDate = getScoresFreshnessDate(player); if (scoresFreshnessDate > new Date()) { + log.debug( + `Player "${playerId}" scores are still fresh, skipping. Next refresh on ${formatDate( + scoresFreshnessDate, + )}`, + "ScoresService", + ); - log.debug(`Player "${playerId}" scores are still fresh, skipping. Next refresh on ${formatDate(scoresFreshnessDate)}`, 'ScoresService') - - return {recentPlay: player.recentPlay, newScores: null, scores: convertScoresById(player.playerId, await getPlayerScores(player.playerId))}; + return { + recentPlay: player.recentPlay, + newScores: null, + scores: convertScoresById( + player.playerId, + await getPlayerScores(player.playerId), + ), + }; } } - log.trace(`Fetching player "${playerId}" scores from ScoreSaber...`, 'ScoresService') + log.trace( + `Fetching player "${playerId}" scores from ScoreSaber...`, + "ScoresService", + ); - const updatedPlayerScores = await resolvePromiseOrWaitForPending(`service/updatePlayerScores/${playerId}`, () => updatePlayerScores(player, priority)); + const updatedPlayerScores = await resolvePromiseOrWaitForPending( + `service/updatePlayerScores/${playerId}`, + () => updatePlayerScores(player, priority), + ); if (!updatedPlayerScores) { - log.warn(`Can not refresh player "${playerId}" scores`, 'ScoresService') + log.warn( + `Can not refresh player "${playerId}" scores`, + "ScoresService", + ); return null; } - const {player: updatedPlayer, ...scoresInfo} = updatedPlayerScores; + const { player: updatedPlayer, ...scoresInfo } = updatedPlayerScores; - log.trace(`Player "${playerId}" scores updated`, 'ScoresService', scoresInfo.newScores); + log.trace( + `Player "${playerId}" scores updated`, + "ScoresService", + scoresInfo.newScores, + ); if (scoresInfo.newScores && scoresInfo.newScores.length) { // TODO: update country ranks - eventBus.publish('player-scores-updated', {player: updatedPlayer, ...scoresInfo}); + eventBus.publish("player-scores-updated", { + player: updatedPlayer, + ...scoresInfo, + }); } - log.debug(`Player "${playerId}" refreshing complete.`, 'ScoresService'); + log.debug(`Player "${playerId}" refreshing complete.`, "ScoresService"); return scoresInfo; } catch (e) { if (throwErrors) throw e; - log.debug(`Player "${playerId}" scores refreshing error${e.toString ? `: ${e.toString()}` : ''}`, 'ScoresService', e) + log.debug( + `Player "${playerId}" scores refreshing error${ + e.toString ? `: ${e.toString()}` : "" + }`, + "ScoresService", + e, + ); return null; - } - finally { - updateInProgress = updateInProgress.filter(pId => pId !== playerId); + } finally { + updateInProgress = updateInProgress.filter((pId) => pId !== playerId); refreshCallCounter--; } - } + }; - const refreshAll = async (force = false, priority = PRIORITY.BG_NORMAL, throwErrors = false) => { - log.trace(`Starting refreshing all players scores${force ? ' (forced)' : ''}...`, 'ScoresService'); + const refreshAll = async ( + force = false, + priority = PRIORITY.BG_NORMAL, + throwErrors = false, + ) => { + log.trace( + `Starting refreshing all players scores${force ? " (forced)" : ""}...`, + "ScoresService", + ); const allActivePlayers = await playerService.getAllActive(); if (!allActivePlayers || !allActivePlayers.length) { - log.trace(`No active players in DB, skipping.`, 'ScoresService'); + log.trace(`No active players in DB, skipping.`, "ScoresService"); return null; } - const allNewScores = await Promise.all(allActivePlayers.map(player => refresh(player.playerId, force, priority, throwErrors))); - const allPlayersWithNewScores = allActivePlayers.map((player, idx) => allNewScores[idx] ? {player, ...allNewScores[idx]} : {player, newScores: null, scores: null, recentPlay: null}) + const allNewScores = await Promise.all( + allActivePlayers.map((player) => + refresh(player.playerId, force, priority, throwErrors), + ), + ); + const allPlayersWithNewScores = allActivePlayers.map((player, idx) => + allNewScores[idx] + ? { player, ...allNewScores[idx] } + : { player, newScores: null, scores: null, recentPlay: null }, + ); - log.trace(`All players scores refreshed.`, 'ScoresService', allPlayersWithNewScores); + log.trace( + `All players scores refreshed.`, + "ScoresService", + allPlayersWithNewScores, + ); return allPlayersWithNewScores; - } + }; const destroyService = () => { serviceCreationCount--; if (serviceCreationCount === 0) { - if(configStoreUnsubscribe) configStoreUnsubscribe(); + if (configStoreUnsubscribe) configStoreUnsubscribe(); if (rankedStoreUnsubscribe) rankedStoreUnsubscribe(); playerService.destroyService(); service = null; } - } + }; service = { isDataForPlayerAvailable, @@ -750,8 +1090,8 @@ export default () => { refreshAll, updateRankAndPpFromTheQueue, destroyService, - convertScoresToObject - } + convertScoresToObject, + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/twitch.js b/src/services/twitch.js index 3b2b875..b3b9b0b 100644 --- a/src/services/twitch.js +++ b/src/services/twitch.js @@ -1,16 +1,23 @@ -import queues from '../network/queues/queues'; -import keyValueRepository from '../db/repository/key-value' -import twitchRepository from '../db/repository/twitch' -import createPlayerService from '../services/scoresaber/player' -import profileApiClient from '../network/clients/twitch/api-profile' -import videosApiClient from '../network/clients/twitch/api-videos' -import eventBus from '../utils/broadcast-channel-pubsub' -import log from '../utils/logger' -import {addToDate, dateFromString, durationToMillis, formatDate, millisToDuration, MINUTE} from '../utils/date' -import {PRIORITY} from '../network/queues/http-queue' -import makePendingPromisePool from '../utils/pending-promises' +import queues from "../network/queues/queues"; +import keyValueRepository from "../db/repository/key-value"; +import twitchRepository from "../db/repository/twitch"; +import createPlayerService from "../services/scoresaber/player"; +import profileApiClient from "../network/clients/twitch/api-profile"; +import videosApiClient from "../network/clients/twitch/api-videos"; +import eventBus from "../utils/broadcast-channel-pubsub"; +import log from "../utils/logger"; +import { + addToDate, + dateFromString, + durationToMillis, + formatDate, + millisToDuration, + MINUTE, +} from "../utils/date"; +import { PRIORITY } from "../network/queues/http-queue"; +import makePendingPromisePool from "../utils/pending-promises"; -const TWITCH_TOKEN_KEY = 'twitchToken'; +const TWITCH_TOKEN_KEY = "twitchToken"; const REFRESH_INTERVAL = 5 * MINUTE; @@ -24,31 +31,38 @@ export default () => { const playerService = createPlayerService(); - const getAuthUrl = (state = '', scopes = '') => queues.TWITCH.getAuthUrl(state, scopes) + const getAuthUrl = (state = "", scopes = "") => + queues.TWITCH.getAuthUrl(state, scopes); const getTwitchTokenFromUrl = () => { - const url = (new URL(document.location)); + const url = new URL(document.location); - const error = url.searchParams.get('error') + const error = url.searchParams.get("error"); if (error) { - const errorMsg = url.searchParams.get('error_description'); + const errorMsg = url.searchParams.get("error_description"); throw new Error(errorMsg ? errorMsg : error); } const hash = url.hash; - if (!hash || !hash.length) throw new Error("Twitch did not return access token") + if (!hash || !hash.length) + throw new Error("Twitch did not return access token"); const accessTokenMatch = /access_token=(.*?)(&|$)/.exec(hash); - if (!accessTokenMatch) throw new Error("Twitch did not return access token") + if (!accessTokenMatch) + throw new Error("Twitch did not return access token"); const stateMatch = /state=(.*?)(&|$)/.exec(hash); - return {accessToken: accessTokenMatch[1], url: stateMatch ? decodeURIComponent(stateMatch[1]) : ''}; - } + return { + accessToken: accessTokenMatch[1], + url: stateMatch ? decodeURIComponent(stateMatch[1]) : "", + }; + }; - const processToken = async accessToken => { + const processToken = async (accessToken) => { // validate token - const tokenValidation = (await queues.TWITCH.validateToken(accessToken)).body; + const tokenValidation = (await queues.TWITCH.validateToken(accessToken)) + .body; const expiresIn = tokenValidation.expires_in * 1000; @@ -58,39 +72,77 @@ export default () => { obtained: new Date(), expires: new Date(Date.now() + expiresIn), expires_in: expiresIn, - } + }; await keyValueRepository().set(twitchToken, TWITCH_TOKEN_KEY); - eventBus.publish('twitch-token-refreshed', twitchToken) + eventBus.publish("twitch-token-refreshed", twitchToken); - return twitchToken - } + return twitchToken; + }; - const getCurrentToken = async () => keyValueRepository().get(TWITCH_TOKEN_KEY, true); + const getCurrentToken = async () => + keyValueRepository().get(TWITCH_TOKEN_KEY, true); - const fetchProfile = async (login, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => { + const fetchProfile = async ( + login, + priority = PRIORITY.FG_LOW, + { fullResponse = false, ...options } = {}, + ) => { const token = await getCurrentToken(); if (!token || !token.expires || token.expires <= new Date()) return null; - return resolvePromiseOrWaitForPending(`profileApiClient/${login}/${fullResponse}`, () => profileApiClient.getProcessed({...options, accessToken: token.accessToken, login, priority, fullResponse})); - } + return resolvePromiseOrWaitForPending( + `profileApiClient/${login}/${fullResponse}`, + () => + profileApiClient.getProcessed({ + ...options, + accessToken: token.accessToken, + login, + priority, + fullResponse, + }), + ); + }; - const fetchVideos = async (userId, priority = PRIORITY.FG_LOW, {fullResponse = false, ...options} = {}) => { + const fetchVideos = async ( + userId, + priority = PRIORITY.FG_LOW, + { fullResponse = false, ...options } = {}, + ) => { const token = await getCurrentToken(); if (!token || !token.expires || token.expires <= new Date()) return null; - return resolvePromiseOrWaitForPending(`videosApiClient/${userId}/${fullResponse}`, () => videosApiClient.getProcessed({...options, accessToken: token.accessToken, userId, priority, fullResponse})); - } + return resolvePromiseOrWaitForPending( + `videosApiClient/${userId}/${fullResponse}`, + () => + videosApiClient.getProcessed({ + ...options, + accessToken: token.accessToken, + userId, + priority, + fullResponse, + }), + ); + }; - const getPlayerProfile = async playerId => twitchRepository().get(playerId); - const updatePlayerProfile = async twitchProfile => twitchRepository().set(twitchProfile); + const getPlayerProfile = async (playerId) => twitchRepository().get(playerId); + const updatePlayerProfile = async (twitchProfile) => + twitchRepository().set(twitchProfile); - const refresh = async (playerId, forceUpdate = false, priority = queues.PRIORITY.FG_LOW, throwErrors = false) => { - log.trace(`Starting Twitch videos refreshing${forceUpdate ? ' (forced)' : ''}...`, 'TwitchService') + const refresh = async ( + playerId, + forceUpdate = false, + priority = queues.PRIORITY.FG_LOW, + throwErrors = false, + ) => { + log.trace( + `Starting Twitch videos refreshing${forceUpdate ? " (forced)" : ""}...`, + "TwitchService", + ); if (!playerId) { - log.debug(`No playerId provided, skipping`, 'TwitchService') + log.debug(`No playerId provided, skipping`, "TwitchService"); return null; } @@ -98,7 +150,10 @@ export default () => { try { let twitchProfile = await twitchRepository().get(playerId); if (!twitchProfile || !twitchProfile.login) { - log.debug(`Twitch profile for player ${playerId} is not set, skipping`, 'TwitchService') + log.debug( + `Twitch profile for player ${playerId} is not set, skipping`, + "TwitchService", + ); return null; } @@ -106,7 +161,12 @@ export default () => { const lastUpdated = twitchProfile.lastUpdated; if (!forceUpdate) { if (lastUpdated && lastUpdated > new Date() - REFRESH_INTERVAL) { - log.debug(`Refresh interval not yet expired, skipping. Next refresh on ${formatDate(addToDate(REFRESH_INTERVAL, lastUpdated))}`, 'TwitchService') + log.debug( + `Refresh interval not yet expired, skipping. Next refresh on ${formatDate( + addToDate(REFRESH_INTERVAL, lastUpdated), + )}`, + "TwitchService", + ); return twitchProfile; } @@ -115,7 +175,10 @@ export default () => { const player = playerService.get(playerId); if (player && player.recentPlay) { if (lastUpdated && lastUpdated > player.recentPlay) { - log.debug(`Twitch updated after recent player play, skipping`, 'TwitchService') + log.debug( + `Twitch updated after recent player play, skipping`, + "TwitchService", + ); return twitchProfile; } @@ -124,12 +187,15 @@ export default () => { if (!twitchProfile.id) { const fetchedProfile = await fetchProfile(twitchProfile.login); if (!fetchedProfile) { - log.debug(`Can not fetch Twitch profile for player ${playerId}, skipping`, 'TwitchService') + log.debug( + `Can not fetch Twitch profile for player ${playerId}, skipping`, + "TwitchService", + ); return twitchProfile; } - twitchProfile = {...twitchProfile, ...fetchedProfile, playerId}; + twitchProfile = { ...twitchProfile, ...fetchedProfile, playerId }; await updatePlayerProfile(twitchProfile); } @@ -141,7 +207,7 @@ export default () => { await updatePlayerProfile(twitchProfile); if (videos && videos.length) { - eventBus.publish('player-twitch-videos-updated', { + eventBus.publish("player-twitch-videos-updated", { playerId, twitchProfile, }); @@ -149,27 +215,48 @@ export default () => { return twitchProfile; } catch (e) { - if (throwErrors) throw e; + if (throwErrors) throw e; - log.debug(`Twitch player ${playerId} refreshing error`, 'TwitchService', e) + log.debug( + `Twitch player ${playerId} refreshing error`, + "TwitchService", + e, + ); - return null; - } - } + return null; + } + }; async function findTwitchVideo(playerTwitchProfile, timeset, songLength) { - if (!playerTwitchProfile || !playerTwitchProfile.videos || !timeset || !songLength) return null; + if ( + !playerTwitchProfile || + !playerTwitchProfile.videos || + !timeset || + !songLength + ) + return null; - const songStarted = addToDate(-songLength * 1000, timeset) + const songStarted = addToDate(-songLength * 1000, timeset); const video = playerTwitchProfile.videos - .map(v => ({ + .map((v) => ({ ...v, created_at: dateFromString(v.created_at), - ended_at: addToDate(durationToMillis(v.duration), dateFromString(v.created_at)), + ended_at: addToDate( + durationToMillis(v.duration), + dateFromString(v.created_at), + ), })) - .find(v => v.created_at <= songStarted && songStarted < v.ended_at); + .find((v) => v.created_at <= songStarted && songStarted < v.ended_at); - return video ? {...video, url: video.url + '?t=' + millisToDuration(songStarted - video.created_at)} : null; + return video + ? { + ...video, + url: + video.url + + "?t=" + + millisToDuration(songStarted - video.created_at), + } + : null; } const destroyService = () => { @@ -179,7 +266,7 @@ export default () => { service = null; playerService.destroyService(); } - } + }; service = { getAuthUrl, @@ -192,7 +279,7 @@ export default () => { findTwitchVideo, refresh, destroyService, - } + }; return service; -} \ No newline at end of file +}; diff --git a/src/services/utils.js b/src/services/utils.js index 2b45649..ff0390c 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -1,43 +1,67 @@ -import {DateTime} from 'luxon'; +import { DateTime } from "luxon"; -export const getServicePlayerGain = (playerHistory, dateTruncFunc, dateKey, daysAgo = 1, maxDaysAgo = 7) => { +export const getServicePlayerGain = ( + playerHistory, + dateTruncFunc, + dateKey, + daysAgo = 1, + maxDaysAgo = 7, +) => { if (!playerHistory?.length) return null; let todayServiceMidnightDate = dateTruncFunc(new Date()); - const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: daysAgo}).toJSDate(); - const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate).minus({days: maxDaysAgo}).toJSDate(); + const compareToDate = DateTime.fromJSDate(todayServiceMidnightDate) + .minus({ days: daysAgo }) + .toJSDate(); + const maxServiceDate = DateTime.fromJSDate(todayServiceMidnightDate) + .minus({ days: maxDaysAgo }) + .toJSDate(); return playerHistory .sort((a, b) => b?.[dateKey]?.getTime() - a?.[dateKey]?.getTime()) - .find(h => h?.[dateKey] <= compareToDate && h?.[dateKey] >= maxServiceDate); -} + .find( + (h) => h?.[dateKey] <= compareToDate && h?.[dateKey] >= maxServiceDate, + ); +}; -export const serviceFilterFunc = serviceParams => s => { +export const serviceFilterFunc = (serviceParams) => (s) => { // accept score if there is no non-empty filter - if (!Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val)?.length) return true; + if ( + !Object.entries(serviceParams?.filters ?? {})?.filter(([key, val]) => val) + ?.length + ) + return true; let filterVal = true; if (serviceParams?.filters?.search?.length) { const song = s?.leaderboard?.song ?? null; if (song) { - const name = `${song?.name?.toLowerCase() ?? ''} ${song?.subName?.toLowerCase() ?? ''} ${song?.authorName?.toLowerCase() ?? ''} ${song?.levelAuthorName?.toLowerCase() ?? ''}` + const name = `${song?.name?.toLowerCase() ?? ""} ${ + song?.subName?.toLowerCase() ?? "" + } ${song?.authorName?.toLowerCase() ?? ""} ${ + song?.levelAuthorName?.toLowerCase() ?? "" + }`; - filterVal &= name.indexOf(serviceParams.filters.search.toLowerCase()) >= 0; + filterVal &= + name.indexOf(serviceParams.filters.search.toLowerCase()) >= 0; } else { filterVal &= false; } } if (serviceParams?.filters.diff?.length) { - filterVal &= s?.leaderboard?.diffInfo?.diff?.toLowerCase() === serviceParams.filters.diff?.toLowerCase() + filterVal &= + s?.leaderboard?.diffInfo?.diff?.toLowerCase() === + serviceParams.filters.diff?.toLowerCase(); } if (serviceParams?.filters?.songType?.length) { - filterVal &= (serviceParams.filters.songType === 'ranked' && s?.pp > 0) || - (serviceParams.filters.songType === 'unranked' && (s?.pp ?? 0) === 0) + filterVal &= + (serviceParams.filters.songType === "ranked" && s?.pp > 0) || + (serviceParams.filters.songType === "unranked" && (s?.pp ?? 0) === 0); } return filterVal; -} \ No newline at end of file +}; diff --git a/src/stores/config.js b/src/stores/config.js index 34e2410..f8e4e62 100644 --- a/src/stores/config.js +++ b/src/stores/config.js @@ -1,21 +1,22 @@ -import {writable} from 'svelte/store' -import keyValueRepository from '../db/repository/key-value'; -import {opt} from '../utils/js' +import { writable } from "svelte/store"; +import keyValueRepository from "../db/repository/key-value"; +import { opt } from "../utils/js"; -const STORE_CONFIG_KEY = 'config'; +const STORE_CONFIG_KEY = "config"; -export const DEFAULT_LOCALE = 'en-US'; +export const DEFAULT_LOCALE = "en-US"; export let configStore = null; const locales = { - 'de-DE': {id: 'de-DE', name: 'Deutschland'}, - 'es-ES': {id: 'es-ES', name: 'España'}, - 'pl-PL': {id: 'pl-PL', name: 'Polska'}, - 'en-GB': {id: 'en-GB', name: 'United Kingdom'}, - 'en-US': {id: 'en-US', name: 'United States'}, + "de-DE": { id: "de-DE", name: "Deutschland" }, + "es-ES": { id: "es-ES", name: "España" }, + "pl-PL": { id: "pl-PL", name: "Polska" }, + "en-GB": { id: "en-GB", name: "United Kingdom" }, + "en-US": { id: "en-US", name: "United States" }, }; -export const getCurrentLocale = () => configStore ? configStore.getLocale() : DEFAULT_LOCALE; +export const getCurrentLocale = () => + configStore ? configStore.getLocale() : DEFAULT_LOCALE; export const getSupportedLocales = () => Object.values(locales); const DEFAULT_CONFIG = { @@ -24,41 +25,42 @@ const DEFAULT_CONFIG = { country: null, }, scoreComparison: { - method: 'in-place', + method: "in-place", }, preferences: { - secondaryPp: 'attribution', - avatarIcons: 'only-if-needed', + secondaryPp: "attribution", + avatarIcons: "only-if-needed", }, locale: DEFAULT_LOCALE, -} +}; const newSettingsAvailableDefinition = { - 'scoreComparison.method': 'Method of displaying the comparison of scores', - 'preferences.secondaryPp': 'Setting the second PP metric', - 'preferences.avatarIcons': 'Showing icons on avatars', - 'locale': 'Locale selection', -} + "scoreComparison.method": "Method of displaying the comparison of scores", + "preferences.secondaryPp": "Setting the second PP metric", + "preferences.avatarIcons": "Showing icons on avatars", + locale: "Locale selection", +}; export default async () => { if (configStore) return configStore; - let currentConfig = {...DEFAULT_CONFIG}; + let currentConfig = { ...DEFAULT_CONFIG }; let newSettingsAvailable = undefined; - const {subscribe, set: storeSet} = writable(currentConfig); + const { subscribe, set: storeSet } = writable(currentConfig); - const get = key => key ? (currentConfig[key] ? currentConfig[key] : null) : currentConfig; + const get = (key) => + key ? (currentConfig[key] ? currentConfig[key] : null) : currentConfig; const set = async (config, persist = true) => { - const newConfig = {...DEFAULT_CONFIG}; - Object.keys(config).forEach(key => { - if (key === 'locale') { + const newConfig = { ...DEFAULT_CONFIG }; + Object.keys(config).forEach((key) => { + if (key === "locale") { newConfig[key] = config?.[key] ?? newConfig?.[key] ?? DEFAULT_LOCALE; return; } - newConfig[key] = {...newConfig?.[key], ...config?.[key]} + newConfig[key] = { ...newConfig?.[key], ...config?.[key] }; }); if (persist) await keyValueRepository().set(newConfig, STORE_CONFIG_KEY); @@ -69,27 +71,31 @@ export default async () => { storeSet(newConfig); return newConfig; - } + }; - const getLocale = () => opt(currentConfig, 'locale', DEFAULT_LOCALE); + const getLocale = () => opt(currentConfig, "locale", DEFAULT_LOCALE); - const determineNewSettingsAvailable = dbConfig => Object.entries(newSettingsAvailableDefinition) - .map(([key, description]) => opt(dbConfig, key) === undefined ? description : null) - .filter(d => d) + const determineNewSettingsAvailable = (dbConfig) => + Object.entries(newSettingsAvailableDefinition) + .map(([key, description]) => + opt(dbConfig, key) === undefined ? description : null, + ) + .filter((d) => d); const dbConfig = await keyValueRepository().get(STORE_CONFIG_KEY); - const newSettings= determineNewSettingsAvailable(dbConfig); + const newSettings = determineNewSettingsAvailable(dbConfig); if (dbConfig) await set(dbConfig, false); - newSettingsAvailable = newSettings && newSettings.length ? newSettings : undefined; + newSettingsAvailable = + newSettings && newSettings.length ? newSettings : undefined; - configStore = { + configStore = { subscribe, set, get, - getMainPlayerId: () => opt(currentConfig, 'users.main'), + getMainPlayerId: () => opt(currentConfig, "users.main"), getLocale, getNewSettingsAvailable: () => newSettingsAvailable, - } + }; return configStore; -} \ No newline at end of file +}; diff --git a/src/stores/container.js b/src/stores/container.js index e41af0a..f918580 100644 --- a/src/stores/container.js +++ b/src/stores/container.js @@ -1,8 +1,10 @@ -import {writable} from 'svelte/store' +import { writable } from "svelte/store"; -export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => { - const defaultValue = {name: null, width: null, nodeWidth: null, rect: null} - const {subscribe, unsubscribe, set} = writable(defaultValue); +export default ( + sizes = { phone: 0, tablet: 768, desktop: 1024, xxl: 1749 }, +) => { + const defaultValue = { name: null, width: null, nodeWidth: null, rect: null }; + const { subscribe, unsubscribe, set } = writable(defaultValue); let ro = null; let node = null; @@ -10,12 +12,12 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => { const unobserve = () => { if (!node) return; - ro.unobserve(node) + ro.unobserve(node); node = null; - } + }; - const observe = nodeToObserve => { + const observe = (nodeToObserve) => { if (!nodeToObserve) return null; if (node) unobserve(); @@ -34,19 +36,25 @@ export default (sizes = {phone: 0, tablet: 768, desktop: 1024, xxl: 1749}) => { set( Object.entries(sizes) .sort((a, b) => a[1] - b[1]) - .reduce((cum, item) => item[1] <= nodeWidth ? {name: item[0], width: item[1], nodeWidth, rect} : cum, defaultValue), - ) + .reduce( + (cum, item) => + item[1] <= nodeWidth + ? { name: item[0], width: item[1], nodeWidth, rect } + : cum, + defaultValue, + ), + ); }); - ro.observe(node) + ro.observe(node); return node; - } + }; return { subscribe, unsubscribe, observe, unobserve, - } + }; }; diff --git a/src/stores/http/enhancers/common/acc-calc.js b/src/stores/http/enhancers/common/acc-calc.js index 37dd128..8f1ca00 100644 --- a/src/stores/http/enhancers/common/acc-calc.js +++ b/src/stores/http/enhancers/common/acc-calc.js @@ -1,12 +1,18 @@ -import {getFixedLeaderboardMaxScore, getMaxScore} from '../../../../utils/scoresaber/song' +import { + getFixedLeaderboardMaxScore, + getMaxScore, +} from "../../../../utils/scoresaber/song"; export default (score, bmStats, leaderboardId) => { let maxScore; if (bmStats && bmStats.notes) { - maxScore = getMaxScore(bmStats.notes) + maxScore = getMaxScore(bmStats.notes); } else if (leaderboardId) { - maxScore = getFixedLeaderboardMaxScore(leaderboardId, score?.maxScore ?? null) + maxScore = getFixedLeaderboardMaxScore( + leaderboardId, + score?.maxScore ?? null, + ); } if (maxScore) { @@ -17,14 +23,14 @@ export default (score, bmStats, leaderboardId) => { if (!unmodifiedScore) unmodifiedScore = score?.score ?? null; if (unmodifiedScore && score.maxScore) { - score.acc = unmodifiedScore ? unmodifiedScore / maxScore * 100 : null; + score.acc = unmodifiedScore ? (unmodifiedScore / maxScore) * 100 : null; - if (score.score) score.percentage = score.score / score.maxScore * 100; + if (score.score) score.percentage = (score.score / score.maxScore) * 100; } if (score?.score && score?.maxScore) { - score.percentage = score.score / score.maxScore * 100; + score.percentage = (score.score / score.maxScore) * 100; } return score; -} \ No newline at end of file +}; diff --git a/src/stores/http/enhancers/common/beatmaps.js b/src/stores/http/enhancers/common/beatmaps.js index 41c426f..02971e6 100644 --- a/src/stores/http/enhancers/common/beatmaps.js +++ b/src/stores/http/enhancers/common/beatmaps.js @@ -1,10 +1,14 @@ -import createBeatMapsService from '../../../../services/beatmaps' -import {opt} from '../../../../utils/js' +import createBeatMapsService from "../../../../services/beatmaps"; +import { opt } from "../../../../utils/js"; const beatMaps = createBeatMapsService(); export default async (data, cachedOnly = false) => { - if (!opt(data, 'leaderboard.song.hash.length')) return; + if (!opt(data, "leaderboard.song.hash.length")) return; - data.leaderboard.beatMaps = await beatMaps.byHash(data.leaderboard.song.hash, false, cachedOnly); -} \ No newline at end of file + data.leaderboard.beatMaps = await beatMaps.byHash( + data.leaderboard.song.hash, + false, + cachedOnly, + ); +}; diff --git a/src/stores/http/enhancers/leaderboard/rankeds.js b/src/stores/http/enhancers/leaderboard/rankeds.js index 65d78f5..afcd086 100644 --- a/src/stores/http/enhancers/leaderboard/rankeds.js +++ b/src/stores/http/enhancers/leaderboard/rankeds.js @@ -1,17 +1,20 @@ -import createRankedsStore from '../../../../stores/scoresaber/rankeds' -import {opt} from '../../../../utils/js' +import createRankedsStore from "../../../../stores/scoresaber/rankeds"; +import { opt } from "../../../../utils/js"; let rankeds; export default async (data) => { - if (rankeds === undefined) { - rankeds = (await createRankedsStore()).get(); - } + if (rankeds === undefined) { + rankeds = (await createRankedsStore()).get(); + } - if (!rankeds) return; + if (!rankeds) return; - const leaderboardId = opt(data, 'leaderboard.leaderboardId'); - if (!leaderboardId) return; + const leaderboardId = opt(data, "leaderboard.leaderboardId"); + if (!leaderboardId) return; - data.leaderboard.stars = rankeds[leaderboardId] && rankeds[leaderboardId].stars ? rankeds[leaderboardId].stars : null; -} \ No newline at end of file + data.leaderboard.stars = + rankeds[leaderboardId] && rankeds[leaderboardId].stars + ? rankeds[leaderboardId].stars + : null; +}; diff --git a/src/stores/http/enhancers/scores/acc.js b/src/stores/http/enhancers/scores/acc.js index 599851c..2ec1888 100644 --- a/src/stores/http/enhancers/scores/acc.js +++ b/src/stores/http/enhancers/scores/acc.js @@ -1,16 +1,22 @@ -import {opt} from '../../../../utils/js' -import calculateAcc from '../common/acc-calc' -import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song' +import { opt } from "../../../../utils/js"; +import calculateAcc from "../common/acc-calc"; +import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song"; export default async (data) => { if (!data || !data.score) return; - const leaderboardId = opt(data, 'leaderboard.leaderboardId') - const diffInfo = opt(data, 'leaderboard.diffInfo'); + const leaderboardId = opt(data, "leaderboard.leaderboardId"); + const diffInfo = opt(data, "leaderboard.diffInfo"); - const versions = opt(data, 'leaderboard.beatMaps.versions') - const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0; - const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo); + const versions = opt(data, "leaderboard.beatMaps.versions"); + const versionsLastIdx = + versions && Array.isArray(versions) && versions.length + ? versions.length - 1 + : 0; + const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps( + opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), + diffInfo, + ); data.score = calculateAcc(data.score, bmStats, leaderboardId); -} \ No newline at end of file +}; diff --git a/src/stores/http/enhancers/scores/beatsavior.js b/src/stores/http/enhancers/scores/beatsavior.js index 9536f36..faf0479 100644 --- a/src/stores/http/enhancers/scores/beatsavior.js +++ b/src/stores/http/enhancers/scores/beatsavior.js @@ -1,6 +1,6 @@ -import createBeatSaviorService from '../../../../services/beatsavior' -import {opt} from '../../../../utils/js' -import {PRIORITY} from '../../../../network/queues/http-queue' +import createBeatSaviorService from "../../../../services/beatsavior"; +import { opt } from "../../../../utils/js"; +import { PRIORITY } from "../../../../network/queues/http-queue"; let beatSaviorService; @@ -15,19 +15,19 @@ export default async (data, playerId = null) => { if (!bsData) return; if (bsData?.stats) - ['left', 'right'].forEach(hand => { - ['Preswing', 'Postswing'].forEach(stat => { + ["left", "right"].forEach((hand) => { + ["Preswing", "Postswing"].forEach((stat) => { const key = `${hand}${stat}`; if (!bsData?.stats?.[key]) bsData.stats[key] = bsData?.trackers?.accuracyTracker?.[key] ?? null; - }) - }) + }); + }); - const acc = opt(bsData, 'trackers.scoreTracker.rawRatio'); + const acc = opt(bsData, "trackers.scoreTracker.rawRatio"); if (acc) data.score.acc = acc * 100; - const percentage = opt(bsData, 'trackers.scoreTracker.modifiedRatio'); + const percentage = opt(bsData, "trackers.scoreTracker.modifiedRatio"); if (percentage) data.score.percentage = percentage * 100; data.beatSavior = bsData; -} \ No newline at end of file +}; diff --git a/src/stores/http/enhancers/scores/compare.js b/src/stores/http/enhancers/scores/compare.js index 14ac6c8..5c02ef7 100644 --- a/src/stores/http/enhancers/scores/compare.js +++ b/src/stores/http/enhancers/scores/compare.js @@ -1,10 +1,10 @@ -import {configStore} from '../../../config' -import createScoresService from '../../../../services/scoresaber/scores' -import accEnhancer from './acc' -import beatSaviorEnhancer from './beatsavior' -import beatMapsEnhancer from '../common/beatmaps' -import {opt} from '../../../../utils/js' -import produce from 'immer' +import { configStore } from "../../../config"; +import createScoresService from "../../../../services/scoresaber/scores"; +import accEnhancer from "./acc"; +import beatSaviorEnhancer from "./beatsavior"; +import beatMapsEnhancer from "../common/beatmaps"; +import { opt } from "../../../../utils/js"; +import produce from "immer"; let scoresService = null; let mainPlayerId = null; @@ -17,20 +17,29 @@ export const initCompareEnhancer = async () => { scoresService = createScoresService(); - configStoreUnsubscribe = configStore.subscribe(async config => { - const newMainPlayerId = opt(config, 'users.main') + configStoreUnsubscribe = configStore.subscribe(async (config) => { + const newMainPlayerId = opt(config, "users.main"); if (mainPlayerId !== newMainPlayerId) { mainPlayerId = newMainPlayerId; - if (!playerScores[mainPlayerId]) playerScores[mainPlayerId] = await scoresService.getPlayerScoresAsObject(mainPlayerId); + if (!playerScores[mainPlayerId]) + playerScores[mainPlayerId] = + await scoresService.getPlayerScoresAsObject(mainPlayerId); } - }) -} + }); +}; export default async (data, playerId = null) => { - if (!data || !data.score || data.comparePlayers || !mainPlayerId || mainPlayerId === playerId) return; + if ( + !data || + !data.score || + data.comparePlayers || + !mainPlayerId || + mainPlayerId === playerId + ) + return; - const leaderboardId = opt(data, 'leaderboard.leaderboardId'); + const leaderboardId = opt(data, "leaderboard.leaderboardId"); if (!leaderboardId) return; const comparePlayerScores = await playerScores[mainPlayerId]; @@ -38,14 +47,15 @@ export default async (data, playerId = null) => { const mainPlayerScore = await produce( await produce( - await produce( - comparePlayerScores[leaderboardId], - draft => beatMapsEnhancer(draft), + await produce(comparePlayerScores[leaderboardId], (draft) => + beatMapsEnhancer(draft), ), - draft => accEnhancer(draft, true), + (draft) => accEnhancer(draft, true), ), - draft => beatSaviorEnhancer(draft, mainPlayerId), + (draft) => beatSaviorEnhancer(draft, mainPlayerId), ); - data.comparePlayers = [{...mainPlayerScore, playerId: mainPlayerId, playerName: 'Me'}]; -} \ No newline at end of file + data.comparePlayers = [ + { ...mainPlayerScore, playerId: mainPlayerId, playerName: "Me" }, + ]; +}; diff --git a/src/stores/http/enhancers/scores/diff.js b/src/stores/http/enhancers/scores/diff.js index 8e6b4ab..f1931a1 100644 --- a/src/stores/http/enhancers/scores/diff.js +++ b/src/stores/http/enhancers/scores/diff.js @@ -1,7 +1,7 @@ -import createScoresService from '../../../../services/scoresaber/scores'; -import calculateAcc from '../common/acc-calc' -import {opt} from '../../../../utils/js' -import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song' +import createScoresService from "../../../../services/scoresaber/scores"; +import calculateAcc from "../common/acc-calc"; +import { opt } from "../../../../utils/js"; +import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song"; let scoresService; @@ -10,27 +10,39 @@ export default async (data, playerId = null) => { if (data.prevScore) delete data.prevScore; - const leaderboardId = opt(data, 'leaderboard.leaderboardId'); + const leaderboardId = opt(data, "leaderboard.leaderboardId"); if (!scoresService) scoresService = createScoresService(); - const playerScores = scoresService.convertScoresToObject(await scoresService.getPlayerScores(playerId)); + const playerScores = scoresService.convertScoresToObject( + await scoresService.getPlayerScores(playerId), + ); // skip if no cached score if (!playerScores[leaderboardId]) return; // compare to cached score if cached is equal to current or to cached history score otherwise - let prevScore = playerScores[leaderboardId].score.score === data.score.score - ? (playerScores[leaderboardId].history && playerScores[leaderboardId].history.length ? playerScores[leaderboardId].history[0] : null) - : playerScores[leaderboardId].score; + let prevScore = + playerScores[leaderboardId].score.score === data.score.score + ? playerScores[leaderboardId].history && + playerScores[leaderboardId].history.length + ? playerScores[leaderboardId].history[0] + : null + : playerScores[leaderboardId].score; // skip if no score to compare if (!prevScore) return; - const diffInfo = opt(data, 'leaderboard.diffInfo'); - const versions = opt(data, 'leaderboard.beatMaps.versions') - const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0; - const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), diffInfo); + const diffInfo = opt(data, "leaderboard.diffInfo"); + const versions = opt(data, "leaderboard.beatMaps.versions"); + const versionsLastIdx = + versions && Array.isArray(versions) && versions.length + ? versions.length - 1 + : 0; + const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps( + opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), + diffInfo, + ); data.prevScore = calculateAcc(prevScore, bmStats, leaderboardId); -} \ No newline at end of file +}; diff --git a/src/stores/http/enhancers/scores/pp-attribution.js b/src/stores/http/enhancers/scores/pp-attribution.js index 613443c..aa01ebb 100644 --- a/src/stores/http/enhancers/scores/pp-attribution.js +++ b/src/stores/http/enhancers/scores/pp-attribution.js @@ -1,24 +1,28 @@ -import createPpService from '../../../../services/scoresaber/pp' -import {configStore} from '../../../config' -import {opt} from '../../../../utils/js' +import createPpService from "../../../../services/scoresaber/pp"; +import { configStore } from "../../../config"; +import { opt } from "../../../../utils/js"; let ppService; export default async (data, playerId = null, whatIfOnly = false) => { if (!playerId) return; - const leaderboardId = opt(data, 'leaderboard.leaderboardId'); + const leaderboardId = opt(data, "leaderboard.leaderboardId"); if (!leaderboardId) return; - const pp = opt(data, 'score.pp'); + const pp = opt(data, "score.pp"); if (!pp) return; if (!ppService) ppService = createPpService(); const mainPlayerId = configStore.getMainPlayerId(); if (mainPlayerId && mainPlayerId !== playerId) { - const whatIfPp = await ppService.getWhatIfScore(mainPlayerId, leaderboardId, pp) - if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp + const whatIfPp = await ppService.getWhatIfScore( + mainPlayerId, + leaderboardId, + pp, + ); + if (whatIfPp && whatIfPp.diff >= 0.01) data.score.whatIfPp = whatIfPp; } if (whatIfOnly) return; @@ -27,4 +31,4 @@ export default async (data, playerId = null, whatIfOnly = false) => { if (!ppAttribution) return; data.score.ppAttribution = -ppAttribution.diff; -} \ No newline at end of file +}; diff --git a/src/stores/http/enhancers/scores/twitch.js b/src/stores/http/enhancers/scores/twitch.js index 86b975d..24ba84a 100644 --- a/src/stores/http/enhancers/scores/twitch.js +++ b/src/stores/http/enhancers/scores/twitch.js @@ -1,15 +1,22 @@ -import createTwitchService from '../../../../services/twitch' -import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../../../utils/scoresaber/song' -import {opt} from '../../../../utils/js' +import createTwitchService from "../../../../services/twitch"; +import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../../../utils/scoresaber/song"; +import { opt } from "../../../../utils/js"; let twitchService; export default async (data, playerId = null) => { - if (!data || !data.score || !data.leaderboard || !data.leaderboard.beatMaps) return; + if (!data || !data.score || !data.leaderboard || !data.leaderboard.beatMaps) + return; - const versions = opt(data, 'leaderboard.beatMaps.versions') - const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0; - const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), data.leaderboard.diffInfo); + const versions = opt(data, "leaderboard.beatMaps.versions"); + const versionsLastIdx = + versions && Array.isArray(versions) && versions.length + ? versions.length - 1 + : 0; + const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps( + opt(data, `leaderboard.beatMaps.versions.${versionsLastIdx}.diffs`), + data.leaderboard.diffInfo, + ); if (!bmStats || !bmStats.seconds) return; if (!twitchService) twitchService = createTwitchService(); @@ -17,8 +24,12 @@ export default async (data, playerId = null) => { const twitchProfile = await twitchService.refresh(playerId); if (!twitchProfile) return; - const video = await twitchService.findTwitchVideo(twitchProfile, data.score.timeSet, bmStats.seconds); + const video = await twitchService.findTwitchVideo( + twitchProfile, + data.score.timeSet, + bmStats.seconds, + ); if (!video || !video.url) return; data.twitchVideo = video; -} \ No newline at end of file +}; diff --git a/src/stores/http/http-leaderboard-store.js b/src/stores/http/http-leaderboard-store.js index 4df2bb1..326e311 100644 --- a/src/stores/http/http-leaderboard-store.js +++ b/src/stores/http/http-leaderboard-store.js @@ -1,44 +1,53 @@ -import createHttpStore from './http-store'; -import beatMapsEnhancer from './enhancers/common/beatmaps' -import accEnhancer from './enhancers/scores/acc' -import createLeaderboardPageProvider from './providers/page-leaderboard' -import {writable} from 'svelte/store' -import {findDiffInfoWithDiffAndTypeFromBeatMaps} from '../../utils/scoresaber/song' -import {debounce} from '../../utils/debounce' -import produce, {applyPatches} from 'immer' -import ppAttributionEnhancer from './enhancers/scores/pp-attribution' +import createHttpStore from "./http-store"; +import beatMapsEnhancer from "./enhancers/common/beatmaps"; +import accEnhancer from "./enhancers/scores/acc"; +import createLeaderboardPageProvider from "./providers/page-leaderboard"; +import { writable } from "svelte/store"; +import { findDiffInfoWithDiffAndTypeFromBeatMaps } from "../../utils/scoresaber/song"; +import { debounce } from "../../utils/debounce"; +import produce, { applyPatches } from "immer"; +import ppAttributionEnhancer from "./enhancers/scores/pp-attribution"; -export default (leaderboardId, type = 'global', page = 1, initialState = null, initialStateType = 'initial') => { +export default ( + leaderboardId, + type = "global", + page = 1, + initialState = null, + initialStateType = "initial", +) => { let currentLeaderboardId = leaderboardId ? leaderboardId : null; - let currentType = type ? type : 'global'; + let currentType = type ? type : "global"; let currentPage = page ? page : 1; - const {subscribe: subscribeEnhanced, set: setEnhanced} = writable(null); + const { subscribe: subscribeEnhanced, set: setEnhanced } = writable(null); - const getCurrentEnhanceTaskId = () => `${currentLeaderboardId}/${currentPage}/${currentType}`; - const getPatchId = (leaderboardId, scoreRow) => `${leaderboardId}/${scoreRow?.player?.playerId}` + const getCurrentEnhanceTaskId = () => + `${currentLeaderboardId}/${currentPage}/${currentType}`; + const getPatchId = (leaderboardId, scoreRow) => + `${leaderboardId}/${scoreRow?.player?.playerId}`; let enhancePatches = {}; let currentEnhanceTaskId = null; - const onNewData = ({fetchParams, state, set}) => { + const onNewData = ({ fetchParams, state, set }) => { currentLeaderboardId = fetchParams?.leaderboardId ?? null; - currentType = fetchParams?.type ?? 'global'; + currentType = fetchParams?.type ?? "global"; currentPage = fetchParams?.page ?? 1; if (!state) return; const enhanceTaskId = getCurrentEnhanceTaskId(); if (currentEnhanceTaskId !== enhanceTaskId) { - enhancePatches = {} + enhancePatches = {}; currentEnhanceTaskId = enhanceTaskId; } - const stateProduce = (state, patchId, producer) => produce(state, producer, patches => { - if (!enhancePatches[patchId]) enhancePatches[patchId] = []; + const stateProduce = (state, patchId, producer) => + produce(state, producer, (patches) => { + if (!enhancePatches[patchId]) enhancePatches[patchId] = []; - enhancePatches[patchId].push(...patches) - }) + enhancePatches[patchId].push(...patches); + }); const debouncedSetState = debounce((enhanceTaskId, state) => { if (currentEnhanceTaskId !== enhanceTaskId) return; @@ -46,78 +55,119 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i set(state); }, 100); - const newState = {...state}; + const newState = { ...state }; const setStateRow = (enhanceTaskId, scoreRow) => { if (currentEnhanceTaskId !== enhanceTaskId) return null; - const patchId = getPatchId(currentLeaderboardId, scoreRow) - const stateRowIdx = newState.scores.findIndex(s => getPatchId(currentLeaderboardId, s) === patchId) + const patchId = getPatchId(currentLeaderboardId, scoreRow); + const stateRowIdx = newState.scores.findIndex( + (s) => getPatchId(currentLeaderboardId, s) === patchId, + ); if (stateRowIdx < 0) return; - newState.scores[stateRowIdx] = applyPatches(newState.scores[stateRowIdx], enhancePatches[patchId]); + newState.scores[stateRowIdx] = applyPatches( + newState.scores[stateRowIdx], + enhancePatches[patchId], + ); debouncedSetState(enhanceTaskId, newState); return newState.scores[stateRowIdx]; - } + }; if (newState.leaderboard) beatMapsEnhancer(newState) - .then(_ => { - const versions = newState?.leaderboard?.beatMaps?.versions ?? null - const versionsLastIdx = versions && Array.isArray(versions) && versions.length ? versions.length - 1 : 0; + .then((_) => { + const versions = newState?.leaderboard?.beatMaps?.versions ?? null; + const versionsLastIdx = + versions && Array.isArray(versions) && versions.length + ? versions.length - 1 + : 0; const bpm = newState?.leaderboard?.beatMaps?.metadata?.bpm ?? null; - const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps(newState?.leaderboard?.beatMaps?.versions?.[versionsLastIdx]?.diffs, newState?.leaderboard?.diffInfo); + const bmStats = findDiffInfoWithDiffAndTypeFromBeatMaps( + newState?.leaderboard?.beatMaps?.versions?.[versionsLastIdx]?.diffs, + newState?.leaderboard?.diffInfo, + ); if (!bmStats) return null; - newState.leaderboard.stats = {...newState.leaderboard.stats, ...bmStats, bpm}; + newState.leaderboard.stats = { + ...newState.leaderboard.stats, + ...bmStats, + bpm, + }; - setEnhanced({leaderboardId, type, page, enhancedAt: new Date()}) + setEnhanced({ leaderboardId, type, page, enhancedAt: new Date() }); debouncedSetState(enhanceTaskId, newState); return newState.leaderboard.beatMaps; }) - .then(_ => { + .then((_) => { if (!newState.scores || !newState.scores.length) return; for (const scoreRow of newState.scores) { - stateProduce({ - ...scoreRow, - leaderboard: newState.leaderboard - }, getPatchId(currentLeaderboardId, scoreRow), draft => accEnhancer(draft)) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) - .then(scoreRow => stateProduce({...scoreRow, leaderboard: newState.leaderboard}, getPatchId(currentLeaderboardId, scoreRow), draft => ppAttributionEnhancer(draft, scoreRow?.player?.playerId, true)) + stateProduce( + { + ...scoreRow, + leaderboard: newState.leaderboard, + }, + getPatchId(currentLeaderboardId, scoreRow), + (draft) => accEnhancer(draft), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => + stateProduce( + { ...scoreRow, leaderboard: newState.leaderboard }, + getPatchId(currentLeaderboardId, scoreRow), + (draft) => + ppAttributionEnhancer( + draft, + scoreRow?.player?.playerId, + true, + ), + ), ) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)); } - }) - } + }); + }; const provider = createLeaderboardPageProvider(); const httpStore = createHttpStore( provider, - {leaderboardId, type, page}, + { leaderboardId, type, page }, initialState, { onInitialized: onNewData, onAfterStateChange: onNewData, - onSetPending: ({fetchParams}) => ({...fetchParams}), + onSetPending: ({ fetchParams }) => ({ ...fetchParams }), }, - initialStateType + initialStateType, ); - const fetch = async (leaderboardId = currentLeaderboardId, type = currentType, page = currentPage, force = false) => { + const fetch = async ( + leaderboardId = currentLeaderboardId, + type = currentType, + page = currentPage, + force = false, + ) => { if (!leaderboardId) return false; - if (leaderboardId === currentLeaderboardId && (!type || type === currentType) && (!page || page === currentPage) && !force) return false; + if ( + leaderboardId === currentLeaderboardId && + (!type || type === currentType) && + (!page || page === currentPage) && + !force + ) + return false; - return httpStore.fetch({leaderboardId, type, page}, force, provider); - } + return httpStore.fetch({ leaderboardId, type, page }, force, provider); + }; - const refresh = async () => fetch(currentLeaderboardId, currentType, currentPage, true); + const refresh = async () => + fetch(currentLeaderboardId, currentType, currentPage, true); return { ...httpStore, @@ -126,7 +176,6 @@ export default (leaderboardId, type = 'global', page = 1, initialState = null, i getLeaderboardId: () => currentLeaderboardId, getType: () => currentType, getPage: () => currentPage, - enhanced: {subscribe: subscribeEnhanced}, - } -} - + enhanced: { subscribe: subscribeEnhanced }, + }; +}; diff --git a/src/stores/http/http-player-store.js b/src/stores/http/http-player-store.js index 7c8c238..0d7351d 100644 --- a/src/stores/http/http-player-store.js +++ b/src/stores/http/http-player-store.js @@ -1,16 +1,20 @@ -import createHttpStore from './http-store'; -import playerApiClient from '../../network/clients/scoresaber/player/api' +import createHttpStore from "./http-store"; +import playerApiClient from "../../network/clients/scoresaber/player/api"; -export default (playerId = null, initialState = null, initialStateType = 'initial') => { +export default ( + playerId = null, + initialState = null, + initialStateType = "initial", +) => { let currentPlayerId = playerId; - const onNewData = ({fetchParams}) => { + const onNewData = ({ fetchParams }) => { currentPlayerId = fetchParams?.playerId ?? null; - } + }; const httpStore = createHttpStore( playerApiClient, - playerId ? {playerId} : null, + playerId ? { playerId } : null, initialState, { onInitialized: onNewData, @@ -22,13 +26,12 @@ export default (playerId = null, initialState = null, initialStateType = 'initia const fetch = async (playerId = currentPlayerId, force = false) => { if (!playerId || (playerId === currentPlayerId && !force)) return false; - return httpStore.fetch({playerId}, force, playerApiClient); - } + return httpStore.fetch({ playerId }, force, playerApiClient); + }; return { ...httpStore, fetch, getPlayerId: () => currentPlayerId, - } -} - + }; +}; diff --git a/src/stores/http/http-player-with-scores-store.js b/src/stores/http/http-player-with-scores-store.js index 33c9577..e109770 100644 --- a/src/stores/http/http-player-with-scores-store.js +++ b/src/stores/http/http-player-with-scores-store.js @@ -1,49 +1,61 @@ -import stringify from 'json-stable-stringify'; -import eventBus from '../../utils/broadcast-channel-pubsub' -import createHttpStore from './http-store'; -import createApiPlayerWithScoresProvider from './providers/api-player-with-scores' -import createPlayerService from '../../services/scoresaber/player' -import {addToDate, MINUTE} from '../../utils/date' -import {writable} from 'svelte/store' +import stringify from "json-stable-stringify"; +import eventBus from "../../utils/broadcast-channel-pubsub"; +import createHttpStore from "./http-store"; +import createApiPlayerWithScoresProvider from "./providers/api-player-with-scores"; +import createPlayerService from "../../services/scoresaber/player"; +import { addToDate, MINUTE } from "../../utils/date"; +import { writable } from "svelte/store"; -export default (playerId = null, service = 'scoresaber', serviceParams = {type: 'recent', page: 1}, initialState = null, initialStateType = 'initial') => { +export default ( + playerId = null, + service = "scoresaber", + serviceParams = { type: "recent", page: 1 }, + initialState = null, + initialStateType = "initial", +) => { let currentPlayerId = playerId; let currentService = service; let currentServiceParams = serviceParams; - const {subscribe: subscribeParams, set: setParams} = writable(null); + const { subscribe: subscribeParams, set: setParams } = writable(null); let playerService = createPlayerService(); let lastRecentPlay = null; let playerForLastRecentPlay = null; - const onNewData = ({fetchParams}) => { + const onNewData = ({ fetchParams }) => { currentPlayerId = fetchParams?.playerId ?? null; currentService = fetchParams?.service ?? null; currentServiceParams = fetchParams?.serviceParams ?? null; - setParams({currentPlayerId, currentService, currentServiceParams}) - } + setParams({ currentPlayerId, currentService, currentServiceParams }); + }; const provider = createApiPlayerWithScoresProvider(); const httpStore = createHttpStore( provider, - playerId ? {playerId, service, serviceParams} : null, + playerId ? { playerId, service, serviceParams } : null, initialState, { onInitialized: onNewData, onAfterStateChange: onNewData, }, - initialStateType + initialStateType, ); - const fetch = async (playerId = currentPlayerId, service = currentService, serviceParams = currentServiceParams, force = false) => { + const fetch = async ( + playerId = currentPlayerId, + service = currentService, + serviceParams = currentServiceParams, + force = false, + ) => { if ( (!playerId || playerId === currentPlayerId) && (!service || stringify(service) === stringify(currentService)) && - (!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) && + (!serviceParams || + stringify(serviceParams) === stringify(currentServiceParams)) && !force ) return false; @@ -54,75 +66,89 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type: playerForLastRecentPlay = playerId; } - return httpStore.fetch({playerId, service, serviceParams}, force, provider, !playerId || playerId !== currentPlayerId || force); - } + return httpStore.fetch( + { playerId, service, serviceParams }, + force, + provider, + !playerId || playerId !== currentPlayerId || force, + ); + }; - const refresh = async () => fetch(currentPlayerId, currentService, currentServiceParams, true); + const refresh = async () => + fetch(currentPlayerId, currentService, currentServiceParams, true); - const playerRecentPlayUpdatedUnsubscribe = eventBus.on('player-recent-play-updated', async ({playerId, recentPlay}) => { - if (!playerId || !currentPlayerId || playerId !== currentPlayerId) return; + const playerRecentPlayUpdatedUnsubscribe = eventBus.on( + "player-recent-play-updated", + async ({ playerId, recentPlay }) => { + if (!playerId || !currentPlayerId || playerId !== currentPlayerId) return; - if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) { - if (recentPlay) { - lastRecentPlay = recentPlay; - playerForLastRecentPlay = playerId; + if (!recentPlay || !lastRecentPlay || recentPlay <= lastRecentPlay) { + if (recentPlay) { + lastRecentPlay = recentPlay; + playerForLastRecentPlay = playerId; + } + return; } - return; - } - lastRecentPlay = recentPlay; - playerForLastRecentPlay = playerId; + lastRecentPlay = recentPlay; + playerForLastRecentPlay = playerId; - await refresh(); - }); + await refresh(); + }, + ); - const subscribe = fn => { + const subscribe = (fn) => { const storeUnsubscribe = httpStore.subscribe(fn); return () => { storeUnsubscribe(); playerRecentPlayUpdatedUnsubscribe(); - } - } + }; + }; const DEFAULT_RECENT_PLAY_REFRESH_INTERVAL = MINUTE; const enqueueRecentPlayRefresh = async () => { if (!currentPlayerId) { - setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL); + setTimeout( + () => enqueueRecentPlayRefresh(), + DEFAULT_RECENT_PLAY_REFRESH_INTERVAL, + ); return; } await playerService.fetchPlayerAndUpdateRecentPlay(currentPlayerId); - const refreshInterval = !lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date()) - ? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL - : 15 * MINUTE; + const refreshInterval = + !lastRecentPlay || lastRecentPlay >= addToDate(-30 * MINUTE, new Date()) + ? DEFAULT_RECENT_PLAY_REFRESH_INTERVAL + : 15 * MINUTE; setTimeout(() => enqueueRecentPlayRefresh(), refreshInterval); + }; - } - - setTimeout(() => enqueueRecentPlayRefresh(), DEFAULT_RECENT_PLAY_REFRESH_INTERVAL); + setTimeout( + () => enqueueRecentPlayRefresh(), + DEFAULT_RECENT_PLAY_REFRESH_INTERVAL, + ); return { ...httpStore, subscribe, fetch, refresh, - params: {subscribe: subscribeParams}, + params: { subscribe: subscribeParams }, getPlayerId: () => currentPlayerId, getService: () => currentService, - setService: type => { + setService: (type) => { currentService = type; - setParams({currentPlayerId, currentService, currentServiceParams}) + setParams({ currentPlayerId, currentService, currentServiceParams }); }, getServiceParams: () => currentServiceParams, - setServiceParams: page => { - currentServiceParams = page - setParams({currentPlayerId, currentService, currentServiceParams}) + setServiceParams: (page) => { + currentServiceParams = page; + setParams({ currentPlayerId, currentService, currentServiceParams }); }, - } -} - + }; +}; diff --git a/src/stores/http/http-ranking-store.js b/src/stores/http/http-ranking-store.js index 152100c..1260d19 100644 --- a/src/stores/http/http-ranking-store.js +++ b/src/stores/http/http-ranking-store.js @@ -1,34 +1,48 @@ -import createHttpStore from './http-store'; -import createApiRankingProvider from './providers/api-ranking' +import createHttpStore from "./http-store"; +import createApiRankingProvider from "./providers/api-ranking"; -export default (type = 'global', page = 1, initialState = null, initialStateType = 'initial') => { - let currentType = type ? type : 'global'; +export default ( + type = "global", + page = 1, + initialState = null, + initialStateType = "initial", +) => { + let currentType = type ? type : "global"; let currentPage = page ? page : 1; - const onNewData = ({fetchParams}) => { - currentType = fetchParams?.type ?? 'global'; + const onNewData = ({ fetchParams }) => { + currentType = fetchParams?.type ?? "global"; currentPage = fetchParams?.page ?? 1; - } + }; const provider = createApiRankingProvider(); const httpStore = createHttpStore( provider, - {type, page}, + { type, page }, initialState, { onInitialized: onNewData, onAfterStateChange: onNewData, - onSetPending: ({fetchParams}) => ({...fetchParams}), + onSetPending: ({ fetchParams }) => ({ ...fetchParams }), }, - initialStateType + initialStateType, ); - const fetch = async (type = currentType, page = currentPage, force = false) => { - if ((!type || type === currentType) && (!page || page === currentPage) && !force) return false; + const fetch = async ( + type = currentType, + page = currentPage, + force = false, + ) => { + if ( + (!type || type === currentType) && + (!page || page === currentPage) && + !force + ) + return false; - return httpStore.fetch({type, page}, force, provider); - } + return httpStore.fetch({ type, page }, force, provider); + }; const refresh = async () => fetch(currentType, currentPage, true); @@ -38,6 +52,5 @@ export default (type = 'global', page = 1, initialState = null, initialStateType refresh, getType: () => currentType, getPage: () => currentPage, - } -} - + }; +}; diff --git a/src/stores/http/http-scores-store.js b/src/stores/http/http-scores-store.js index 7bb06bd..9bff65d 100644 --- a/src/stores/http/http-scores-store.js +++ b/src/stores/http/http-scores-store.js @@ -1,26 +1,34 @@ -import createHttpStore from './http-store'; -import beatMapsEnhancer from './enhancers/common/beatmaps' -import accEnhancer from './enhancers/scores/acc' -import beatSaviorEnhancer from './enhancers/scores/beatsavior' -import rankedsEnhancer from './enhancers/leaderboard/rankeds' -import compareEnhancer from './enhancers/scores/compare' -import diffEnhancer from './enhancers/scores/diff' -import twitchEnhancer from './enhancers/scores/twitch' -import ppAttributionEnhancer from './enhancers/scores/pp-attribution' -import {debounce} from '../../utils/debounce' -import createApiScoresProvider from './providers/api-scores' -import produce, {applyPatches} from 'immer' -import stringify from 'json-stable-stringify' +import createHttpStore from "./http-store"; +import beatMapsEnhancer from "./enhancers/common/beatmaps"; +import accEnhancer from "./enhancers/scores/acc"; +import beatSaviorEnhancer from "./enhancers/scores/beatsavior"; +import rankedsEnhancer from "./enhancers/leaderboard/rankeds"; +import compareEnhancer from "./enhancers/scores/compare"; +import diffEnhancer from "./enhancers/scores/diff"; +import twitchEnhancer from "./enhancers/scores/twitch"; +import ppAttributionEnhancer from "./enhancers/scores/pp-attribution"; +import { debounce } from "../../utils/debounce"; +import createApiScoresProvider from "./providers/api-scores"; +import produce, { applyPatches } from "immer"; +import stringify from "json-stable-stringify"; -export default (playerId = null, service = 'scoresaber', serviceParams = {type: 'recent', page: 1}, initialState = null, initialStateType = 'initial') => { +export default ( + playerId = null, + service = "scoresaber", + serviceParams = { type: "recent", page: 1 }, + initialState = null, + initialStateType = "initial", +) => { let currentPlayerId = playerId; let currentService = service; let currentServiceParams = serviceParams; let totalScores = null; - const getCurrentEnhanceTaskId = () => `${currentPlayerId}/${currentService}/${stringify(currentServiceParams)}`; - const getPatchId = (playerId, scoreRow) => `${playerId}/${scoreRow?.leaderboard?.leaderboardId}` + const getCurrentEnhanceTaskId = () => + `${currentPlayerId}/${currentService}/${stringify(currentServiceParams)}`; + const getPatchId = (playerId, scoreRow) => + `${playerId}/${scoreRow?.leaderboard?.leaderboardId}`; let enhancePatches = {}; let currentEnhanceTaskId = null; @@ -34,9 +42,9 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type: totalScores = state !== null ? null : 0; return state; - } + }; - const onNewData = ({fetchParams, state, stateType, set}) => { + const onNewData = ({ fetchParams, state, stateType, set }) => { currentPlayerId = fetchParams?.playerId ?? null; currentService = fetchParams?.service ?? null; currentServiceParams = fetchParams?.serviceParams ?? null; @@ -50,15 +58,16 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type: const enhanceTaskId = getCurrentEnhanceTaskId(); if (currentEnhanceTaskId !== enhanceTaskId) { - enhancePatches = {} + enhancePatches = {}; currentEnhanceTaskId = enhanceTaskId; } - const stateProduce = (state, patchId, producer) => produce(state, producer, patches => { - if (!enhancePatches[patchId]) enhancePatches[patchId] = []; + const stateProduce = (state, patchId, producer) => + produce(state, producer, (patches) => { + if (!enhancePatches[patchId]) enhancePatches[patchId] = []; - enhancePatches[patchId].push(...patches) - }) + enhancePatches[patchId].push(...patches); + }); const debouncedSetState = debounce((enhanceTaskId, state) => { if (currentEnhanceTaskId !== enhanceTaskId) return; @@ -71,77 +80,139 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type: const setStateRow = (enhanceTaskId, scoreRow) => { if (currentEnhanceTaskId !== enhanceTaskId) return null; - const patchId = getPatchId(currentPlayerId, scoreRow) - const stateRowIdx = newState.findIndex(s => getPatchId(currentPlayerId, s) === patchId) + const patchId = getPatchId(currentPlayerId, scoreRow); + const stateRowIdx = newState.findIndex( + (s) => getPatchId(currentPlayerId, s) === patchId, + ); if (stateRowIdx < 0) return; - newState[stateRowIdx] = applyPatches(newState[stateRowIdx], enhancePatches[patchId]); + newState[stateRowIdx] = applyPatches( + newState[stateRowIdx], + enhancePatches[patchId], + ); debouncedSetState(enhanceTaskId, newState); return newState[stateRowIdx]; - } + }; for (const scoreRow of newState) { - if (currentService !== 'accsaber') { - stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatMapsEnhancer(draft)) - .then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => accEnhancer(draft))) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) - .then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => diffEnhancer(draft, currentPlayerId))) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) - .then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => compareEnhancer(draft, currentPlayerId))) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) - .then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => twitchEnhancer(draft, currentPlayerId))) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) + if (currentService !== "accsaber") { + stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) => + beatMapsEnhancer(draft), + ) + .then((scoreRow) => + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => accEnhancer(draft), + ), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => diffEnhancer(draft, currentPlayerId), + ), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => compareEnhancer(draft, currentPlayerId), + ), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => twitchEnhancer(draft, currentPlayerId), + ), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)); - stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => rankedsEnhancer(draft)) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) + stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) => + rankedsEnhancer(draft), + ).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)); - stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => ppAttributionEnhancer(draft, currentPlayerId)) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) + stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) => + ppAttributionEnhancer(draft, currentPlayerId), + ).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)); - if (stateType && stateType === 'live') - stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatSaviorEnhancer(draft, currentPlayerId)) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) + if (stateType && stateType === "live") + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => beatSaviorEnhancer(draft, currentPlayerId), + ).then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)); } else { - stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatMapsEnhancer(draft)) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) - .then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => twitchEnhancer(draft, currentPlayerId))) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) - .then(scoreRow => stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), draft => beatSaviorEnhancer(draft, currentPlayerId))) - .then(scoreRow => setStateRow(enhanceTaskId, scoreRow)) + stateProduce(scoreRow, getPatchId(currentPlayerId, scoreRow), (draft) => + beatMapsEnhancer(draft), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => twitchEnhancer(draft, currentPlayerId), + ), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)) + .then((scoreRow) => + stateProduce( + scoreRow, + getPatchId(currentPlayerId, scoreRow), + (draft) => beatSaviorEnhancer(draft, currentPlayerId), + ), + ) + .then((scoreRow) => setStateRow(enhanceTaskId, scoreRow)); } } - } + }; const provider = createApiScoresProvider(); const httpStore = createHttpStore( provider, - playerId ? {playerId, service, serviceParams} : null, + playerId ? { playerId, service, serviceParams } : null, initialState, { onInitialized: onNewData, onBeforeStateChange, onAfterStateChange: onNewData, - onSetPending: ({fetchParams}) => ({...fetchParams}), + onSetPending: ({ fetchParams }) => ({ ...fetchParams }), }, - initialStateType + initialStateType, ); - const fetch = async (serviceParams = currentServiceParams, service = currentService, playerId = currentPlayerId, force = false) => { + const fetch = async ( + serviceParams = currentServiceParams, + service = currentService, + playerId = currentPlayerId, + force = false, + ) => { if ( (!playerId || playerId === currentPlayerId) && (!service || stringify(service) === stringify(currentService)) && - (!serviceParams || stringify(serviceParams) === stringify(currentServiceParams)) && + (!serviceParams || + stringify(serviceParams) === stringify(currentServiceParams)) && !force ) return false; - return httpStore.fetch({playerId, service, serviceParams}, force, provider, !playerId || playerId !== currentPlayerId || force); - } + return httpStore.fetch( + { playerId, service, serviceParams }, + force, + provider, + !playerId || playerId !== currentPlayerId || force, + ); + }; - const refresh = async () => fetch(currentServiceParams, currentService, currentPlayerId, true); + const refresh = async () => + fetch(currentServiceParams, currentService, currentPlayerId, true); return { ...httpStore, @@ -151,6 +222,5 @@ export default (playerId = null, service = 'scoresaber', serviceParams = {type: getService: () => currentService, getServiceParams: () => currentServiceParams, getTotalScores: () => totalScores, - } -} - + }; +}; diff --git a/src/stores/http/http-store.js b/src/stores/http/http-store.js index 61b6af4..4333fb6 100644 --- a/src/stores/http/http-store.js +++ b/src/stores/http/http-store.js @@ -1,8 +1,8 @@ -import {writable} from 'svelte/store' -import stringify from 'json-stable-stringify'; -import {SsrNetworkTimeoutError} from '../../network/errors' +import { writable } from "svelte/store"; +import stringify from "json-stable-stringify"; +import { SsrNetworkTimeoutError } from "../../network/errors"; -const hash = obj => stringify(obj); +const hash = (obj) => stringify(obj); export default ( provider, @@ -16,9 +16,12 @@ export default ( onSetPending = null, onError = null, } = {}, - initialStateType = 'initial' + initialStateType = "initial", ) => { - const getFinalParams = fetchParams => ({...defaultFetchParams, ...fetchParams}); + const getFinalParams = (fetchParams) => ({ + ...defaultFetchParams, + ...fetchParams, + }); let stateType = initialStateType; let state = initialState; @@ -27,18 +30,24 @@ export default ( let currentParams = fetchParams; let currentParamsHash = hash(getFinalParams(fetchParams)); - const setProvider = provider => currentProvider = provider; + const setProvider = (provider) => (currentProvider = provider); - const {subscribe: subscribeState, set} = writable(state); - if (onInitialized) onInitialized({state, stateType, fetchParams, defaultFetchParams, set}); + const { subscribe: subscribeState, set } = writable(state); + if (onInitialized) + onInitialized({ state, stateType, fetchParams, defaultFetchParams, set }); - const {subscribe: subscribeIsLoading, set: setIsLoading} = writable(false); - const {subscribe: subscribePending, set: setPending} = writable(null); - const {subscribe: subscribeError, set: setError} = writable(null); + const { subscribe: subscribeIsLoading, set: setIsLoading } = writable(false); + const { subscribe: subscribePending, set: setPending } = writable(null); + const { subscribe: subscribeError, set: setError } = writable(null); let pendingAbortController; - const fetch = async (fetchParams = {}, force = false, provider = currentProvider, fetchCachedFirst = false) => { + const fetch = async ( + fetchParams = {}, + force = false, + provider = currentProvider, + fetchCachedFirst = false, + ) => { const abortController = new AbortController(); try { @@ -52,37 +61,59 @@ export default ( if (fetchCachedFirst) { const beforeState = state; - provider.getCached(finalParams) - .then(cachedState => { - if (cachedState && beforeState === state) { - state = cachedState; - set(onBeforeStateChange ? onBeforeStateChange(cachedState, stateType) : cachedState); - } - }) + provider.getCached(finalParams).then((cachedState) => { + if (cachedState && beforeState === state) { + state = cachedState; + set( + onBeforeStateChange + ? onBeforeStateChange(cachedState, stateType) + : cachedState, + ); + } + }); } setError(null); setIsLoading(true); - setPending(onSetPending ? onSetPending({fetchParams, abortController}) : fetchParams); + setPending( + onSetPending + ? onSetPending({ fetchParams, abortController }) + : fetchParams, + ); pendingAbortController = abortController; - stateType = 'live'; - state = await provider.getProcessed({...finalParams, signal: abortController.signal, force}); + stateType = "live"; + state = await provider.getProcessed({ + ...finalParams, + signal: abortController.signal, + force, + }); currentParams = fetchParams; currentParamsHash = hash(finalParams); - set(onBeforeStateChange ? onBeforeStateChange(state, stateType) : state) + set(onBeforeStateChange ? onBeforeStateChange(state, stateType) : state); - if (onAfterStateChange) onAfterStateChange({state, stateType, fetchParams: currentParams, defaultFetchParams, set}); + if (onAfterStateChange) + onAfterStateChange({ + state, + stateType, + fetchParams: currentParams, + defaultFetchParams, + set, + }); return true; } catch (err) { - if ([err?.name, err?.message].includes('AbortError')) return false; + if ([err?.name, err?.message].includes("AbortError")) return false; try { - if (err instanceof SsrNetworkTimeoutError && abortController && !abortController.aborted) { + if ( + err instanceof SsrNetworkTimeoutError && + abortController && + !abortController.aborted + ) { abortController.abort(); } } catch (e) { @@ -100,19 +131,20 @@ export default ( } return false; - } + }; - if (!initialState && fetchParams) fetch(fetchParams, true, currentProvider, true); + if (!initialState && fetchParams) + fetch(fetchParams, true, currentProvider, true); - const subscribe = fn => { + const subscribe = (fn) => { const stateUnsubscribe = subscribeState(fn); return () => { stateUnsubscribe(); if (currentProvider.destroy) currentProvider.destroy(); - } - } + }; + }; return { subscribe, @@ -122,9 +154,8 @@ export default ( getProvider: () => currentProvider, getParams: () => currentParams, setProvider, - isLoading: {subscribe: subscribeIsLoading}, - pending: {subscribe: subscribePending}, - error: {subscribe: subscribeError}, - } -} - + isLoading: { subscribe: subscribeIsLoading }, + pending: { subscribe: subscribePending }, + error: { subscribe: subscribeError }, + }; +}; diff --git a/src/stores/http/providers/api-player-with-scores.js b/src/stores/http/providers/api-player-with-scores.js index 626c7af..d552728 100644 --- a/src/stores/http/providers/api-player-with-scores.js +++ b/src/stores/http/providers/api-player-with-scores.js @@ -1,8 +1,8 @@ -import createPlayerService from '../../../services/scoresaber/player'; -import createScoresFetcher from './utils/scores-fetch' -import queue from '../../../network/queues/queues' -import {MINUTE, SECOND} from '../../../utils/date' -import {worker} from '../../../utils/worker-wrappers' +import createPlayerService from "../../../services/scoresaber/player"; +import createScoresFetcher from "./utils/scores-fetch"; +import queue from "../../../network/queues/queues"; +import { MINUTE, SECOND } from "../../../utils/date"; +import { worker } from "../../../utils/worker-wrappers"; let playerService = null; let scoresFetcher = null; @@ -14,32 +14,54 @@ export default () => { let firstFetch = true; return { - getProcessed: async ({playerId, priority = queue.PRIORITY.FG_HIGH, service = 'scoresaber', serviceParams = {sort: 'recent', order: 'desc', page: 1}, signal = null, force = false} = {}) => { + getProcessed: async ({ + playerId, + priority = queue.PRIORITY.FG_HIGH, + service = "scoresaber", + serviceParams = { sort: "recent", order: "desc", page: 1 }, + signal = null, + force = false, + } = {}) => { const refreshInterval = firstFetch ? 5 * SECOND : MINUTE; firstFetch = false; - const player = await playerService.fetchPlayerOrGetFromCache(playerId, refreshInterval, priority, signal, force); + const player = await playerService.fetchPlayerOrGetFromCache( + playerId, + refreshInterval, + priority, + signal, + force, + ); - const scores = await scoresFetcher.fetchLiveScores(player, service, serviceParams, {refreshInterval, priority, signal, force}); + const scores = await scoresFetcher.fetchLiveScores( + player, + service, + serviceParams, + { refreshInterval, priority, signal, force }, + ); - return {...player, scores, service, serviceParams} + return { ...player, scores, service, serviceParams }; }, - getCached: async ({playerId, service = 'scoresaber', serviceParams = {sort: 'recent', order: 'desc', page: 1}} = {}) => { + getCached: async ({ + playerId, + service = "scoresaber", + serviceParams = { sort: "recent", order: "desc", page: 1 }, + } = {}) => { const [player, scores] = await Promise.all([ playerService.get(playerId), - scoresFetcher.fetchCachedScores(playerId, service, serviceParams) + scoresFetcher.fetchCachedScores(playerId, service, serviceParams), ]); if (!player || !scores) return null; if (worker) worker.calcPlayerStats(playerId); - return {...player, scores, service, serviceParams} + return { ...player, scores, service, serviceParams }; }, destroy() { // TODO: destroy scoresFetcher & playerService }, - } -} \ No newline at end of file + }; +}; diff --git a/src/stores/http/providers/api-ranking.js b/src/stores/http/providers/api-ranking.js index 0c5d588..2f1ac64 100644 --- a/src/stores/http/providers/api-ranking.js +++ b/src/stores/http/providers/api-ranking.js @@ -1,6 +1,6 @@ -import createRankingService from '../../../services/scoresaber/ranking'; -import queue from '../../../network/queues/queues' -import {addToDate, HOUR} from '../../../utils/date' +import createRankingService from "../../../services/scoresaber/ranking"; +import queue from "../../../network/queues/queues"; +import { addToDate, HOUR } from "../../../utils/date"; const PAGES_REFRESH_INTERVAL = HOUR; @@ -11,23 +11,35 @@ let total = null; export default () => { if (!rankingService) rankingService = createRankingService(); - const getProcessed = async ({type = 'global', page = 1, priority = queue.PRIORITY.FG_HIGH, signal = null, force = false} = {}) => { - if (type === 'global' && (!total || !globalPagesLastRefreshed || addToDate(-PAGES_REFRESH_INTERVAL) > globalPagesLastRefreshed)) { + const getProcessed = async ({ + type = "global", + page = 1, + priority = queue.PRIORITY.FG_HIGH, + signal = null, + force = false, + } = {}) => { + if ( + type === "global" && + (!total || + !globalPagesLastRefreshed || + addToDate(-PAGES_REFRESH_INTERVAL) > globalPagesLastRefreshed) + ) { globalPagesLastRefreshed = new Date(); total = await rankingService.getGlobalCount(priority, signal, force); - } else if (type !== 'global') { + } else if (type !== "global") { total = null; } - const data = type === 'global' - ? await rankingService.getGlobal(page, priority, signal, force) - : await rankingService.getCountry(type, page, priority, signal, force); + const data = + type === "global" + ? await rankingService.getGlobal(page, priority, signal, force) + : await rankingService.getCountry(type, page, priority, signal, force); - return {total, data} - } + return { total, data }; + }; return { getProcessed, - getCached: getProcessed - } -} + getCached: getProcessed, + }; +}; diff --git a/src/stores/http/providers/api-scores.js b/src/stores/http/providers/api-scores.js index 4af8722..821174e 100644 --- a/src/stores/http/providers/api-scores.js +++ b/src/stores/http/providers/api-scores.js @@ -1,8 +1,8 @@ -import createPlayerService from '../../../services/scoresaber/player'; -import createScoresFetcher from './utils/scores-fetch' -import queue from '../../../network/queues/queues' -import {MINUTE} from '../../../utils/date' -import eventBus from '../../../utils/broadcast-channel-pubsub' +import createPlayerService from "../../../services/scoresaber/player"; +import createScoresFetcher from "./utils/scores-fetch"; +import queue from "../../../network/queues/queues"; +import { MINUTE } from "../../../utils/date"; +import eventBus from "../../../utils/broadcast-channel-pubsub"; let playerService = null; let scoresFetcher = null; @@ -13,39 +13,67 @@ export default () => { playerService = createPlayerService(); scoresFetcher = createScoresFetcher(); - const playerProfileChangedUnsubscribe = eventBus.on('player-profile-changed', newPlayer => { - if (!newPlayer || !player || !newPlayer.playerId !== player.playerId) return; + const playerProfileChangedUnsubscribe = eventBus.on( + "player-profile-changed", + (newPlayer) => { + if (!newPlayer || !player || !newPlayer.playerId !== player.playerId) + return; - player = newPlayer; - }); + player = newPlayer; + }, + ); - const playerRecentPlayUpdatedUnsubscribe = eventBus.on('player-recent-play-updated', async ({playerId, recentPlay, recentPlayLastUpdated}) => { - if (!playerId || !player || player.playerId !== playerId) return; + const playerRecentPlayUpdatedUnsubscribe = eventBus.on( + "player-recent-play-updated", + async ({ playerId, recentPlay, recentPlayLastUpdated }) => { + if (!playerId || !player || player.playerId !== playerId) return; - player.recentPlay = recentPlay; - player.recentPlayLastUpdated = recentPlayLastUpdated; - }); + player.recentPlay = recentPlay; + player.recentPlayLastUpdated = recentPlayLastUpdated; + }, + ); return { - async getProcessed({playerId, service = 'scoresaber', serviceParams = {sort: 'recent', order: 'desc', page: 1}, priority = queue.PRIORITY.FG_HIGH, signal = null, force = false} = {}) { + async getProcessed({ + playerId, + service = "scoresaber", + serviceParams = { sort: "recent", order: "desc", page: 1 }, + priority = queue.PRIORITY.FG_HIGH, + signal = null, + force = false, + } = {}) { if (!player || player.playerId !== playerId) - player = await playerService.fetchPlayerOrGetFromCache(playerId, MINUTE, priority, signal); + player = await playerService.fetchPlayerOrGetFromCache( + playerId, + MINUTE, + priority, + signal, + ); - return scoresFetcher.fetchLiveScores(player, service, serviceParams, {refreshInterval: MINUTE, priority, signal, force}); + return scoresFetcher.fetchLiveScores(player, service, serviceParams, { + refreshInterval: MINUTE, + priority, + signal, + force, + }); }, - async getCached({playerId, service = 'scoresaber', serviceParams = {sort: 'recent', order: 'desc', page: 1}} = {}) { + async getCached({ + playerId, + service = "scoresaber", + serviceParams = { sort: "recent", order: "desc", page: 1 }, + } = {}) { return scoresFetcher.fetchCachedScores(playerId, service, serviceParams); }, setPlayer(newPlayer) { - player = newPlayer + player = newPlayer; }, destroy() { playerProfileChangedUnsubscribe(); playerRecentPlayUpdatedUnsubscribe(); // TODO: destroy scoresFetcher & playerService - } - } -} + }, + }; +}; diff --git a/src/stores/http/providers/page-leaderboard.js b/src/stores/http/providers/page-leaderboard.js index ba49b47..025e275 100644 --- a/src/stores/http/providers/page-leaderboard.js +++ b/src/stores/http/providers/page-leaderboard.js @@ -1,21 +1,48 @@ -import createLeaderboardService from '../../../services/scoresaber/leaderboard'; -import queue from '../../../network/queues/queues' +import createLeaderboardService from "../../../services/scoresaber/leaderboard"; +import queue from "../../../network/queues/queues"; let leaderboardService = null; export default () => { if (!leaderboardService) leaderboardService = createLeaderboardService(); - const getProcessed = async ({leaderboardId, type = 'global', page = 1, priority = queue.PRIORITY.FG_HIGH, signal = null, force = false} = {}) => { - switch(type) { - case 'global': return await leaderboardService.fetchPage(leaderboardId, page, priority, signal, force); - case 'accsaber': return await leaderboardService.fetchAccSaberPage(leaderboardId, page, priority, signal, force); - default: return await leaderboardService.getFriendsLeaderboard(leaderboardId, priority, signal, force); + const getProcessed = async ({ + leaderboardId, + type = "global", + page = 1, + priority = queue.PRIORITY.FG_HIGH, + signal = null, + force = false, + } = {}) => { + switch (type) { + case "global": + return await leaderboardService.fetchPage( + leaderboardId, + page, + priority, + signal, + force, + ); + case "accsaber": + return await leaderboardService.fetchAccSaberPage( + leaderboardId, + page, + priority, + signal, + force, + ); + default: + return await leaderboardService.getFriendsLeaderboard( + leaderboardId, + priority, + signal, + force, + ); } - } + }; return { getProcessed, - getCached: getProcessed - } -} + getCached: getProcessed, + }; +}; diff --git a/src/stores/http/providers/utils/scores-fetch.js b/src/stores/http/providers/utils/scores-fetch.js index 0550fd2..257c0a0 100644 --- a/src/stores/http/providers/utils/scores-fetch.js +++ b/src/stores/http/providers/utils/scores-fetch.js @@ -1,6 +1,6 @@ -import createScoresService from '../../../../services/scoresaber/scores' -import createAccSaberService from '../../../../services/accsaber' -import createBeatSaviorService from '../../../../services/beatsavior' +import createScoresService from "../../../../services/scoresaber/scores"; +import createAccSaberService from "../../../../services/accsaber"; +import createBeatSaviorService from "../../../../services/beatsavior"; let scoreFetcher = null; @@ -15,31 +15,54 @@ export default () => { accSaberService = createAccSaberService(); beatSaviorService = createBeatSaviorService(); - const fetchCachedScores = async (playerId, service, serviceParams = {sort: 'recent', order: 'desc', page: 1}, otherParams = {}) => { + const fetchCachedScores = async ( + playerId, + service, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + otherParams = {}, + ) => { switch (service) { - case 'beatsavior': + case "beatsavior": return beatSaviorService.getPlayerScoresPage(playerId, serviceParams); - case 'accsaber': + case "accsaber": return accSaberService.getPlayerScoresPage(playerId, serviceParams); - case 'scoresaber': + case "scoresaber": default: return ssScoresService.getPlayerScoresPage(playerId, serviceParams); } - } + }; - const fetchLiveScores = async (player, service, serviceParams = {sort: 'recent', order: 'desc', page: 1}, otherParams = {}) => { + const fetchLiveScores = async ( + player, + service, + serviceParams = { sort: "recent", order: "desc", page: 1 }, + otherParams = {}, + ) => { switch (service) { - case 'beatsavior': - return beatSaviorService.getPlayerScoresPage(player?.playerId, serviceParams); - case 'accsaber': - return accSaberService.getPlayerScoresPage(player?.playerId, serviceParams); - case 'scoresaber': + case "beatsavior": + return beatSaviorService.getPlayerScoresPage( + player?.playerId, + serviceParams, + ); + case "accsaber": + return accSaberService.getPlayerScoresPage( + player?.playerId, + serviceParams, + ); + case "scoresaber": default: - return ssScoresService.fetchScoresPageOrGetFromCache(player, serviceParams, otherParams?.refreshInterval, otherParams?.priority, otherParams?.signal, otherParams?.force); + return ssScoresService.fetchScoresPageOrGetFromCache( + player, + serviceParams, + otherParams?.refreshInterval, + otherParams?.priority, + otherParams?.signal, + otherParams?.force, + ); } - } + }; - scoreFetcher = {fetchCachedScores, fetchLiveScores} + scoreFetcher = { fetchCachedScores, fetchLiveScores }; return scoreFetcher; -} \ No newline at end of file +}; diff --git a/src/stores/scoresaber/friends.js b/src/stores/scoresaber/friends.js index 600178b..a13cec5 100644 --- a/src/stores/scoresaber/friends.js +++ b/src/stores/scoresaber/friends.js @@ -1,32 +1,32 @@ -import {writable} from 'svelte/store' -import createPlayerStore from './players' -import createPlayerService from '../../services/scoresaber/player' +import { writable } from "svelte/store"; +import createPlayerStore from "./players"; +import createPlayerService from "../../services/scoresaber/player"; export default () => { const playerService = createPlayerService(); - const {subscribe, unsubscribe: stateUnsubscribe, set} = writable([]); + const { subscribe, unsubscribe: stateUnsubscribe, set } = writable([]); const playerStore = createPlayerStore(); - const playerStoreUnsubscribe = playerStore.subscribe(async players => { + const playerStoreUnsubscribe = playerStore.subscribe(async (players) => { const friends = await playerService.getFriends(); set( players - .filter(p => p && p.playerId && friends.includes(p.playerId)) - .sort((a,b) => a.name ? a.name.localeCompare(b.name) : 0) + .filter((p) => p && p.playerId && friends.includes(p.playerId)) + .sort((a, b) => (a.name ? a.name.localeCompare(b.name) : 0)), ); - }) + }); const unsubscribe = () => { stateUnsubscribe(); playerStoreUnsubscribe()(); playerService.destroyService(); - } + }; return { subscribe, unsubscribe, - } + }; }; diff --git a/src/stores/scoresaber/players.js b/src/stores/scoresaber/players.js index 9e4135c..81bb824 100644 --- a/src/stores/scoresaber/players.js +++ b/src/stores/scoresaber/players.js @@ -1,6 +1,6 @@ -import {writable} from 'svelte/store' -import createPlayerService from '../../services/scoresaber/player' -import eventBus from '../../utils/broadcast-channel-pubsub' +import { writable } from "svelte/store"; +import createPlayerService from "../../services/scoresaber/player"; +import eventBus from "../../utils/broadcast-channel-pubsub"; let store = null; let storeSubCount = 0; @@ -11,24 +11,30 @@ export default () => { const playerService = createPlayerService(); let players = []; - const {subscribe: subscribeState, set} = writable(players); + const { subscribe: subscribeState, set } = writable(players); const refreshState = async () => { - players = await playerService.getAll() + players = await playerService.getAll(); - set(players) - } + set(players); + }; const get = () => players; - const playerAddedUnsubscribe = eventBus.on('player-profile-added', refreshState); - const playerRemovedUnsubscribe = eventBus.on('player-profile-removed', refreshState); + const playerAddedUnsubscribe = eventBus.on( + "player-profile-added", + refreshState, + ); + const playerRemovedUnsubscribe = eventBus.on( + "player-profile-removed", + refreshState, + ); - const subscribe = fn => { + const subscribe = (fn) => { const stateUnsubscribe = subscribeState(fn); return () => { - storeSubCount --; + storeSubCount--; if (storeSubCount === 0) { store = null; @@ -39,15 +45,15 @@ export default () => { playerAddedUnsubscribe(); playerRemovedUnsubscribe(); } - } - } + }; + }; - refreshState().then(_ => _); + refreshState().then((_) => _); store = { subscribe, get, - } + }; return store; -} \ No newline at end of file +}; diff --git a/src/stores/scoresaber/rankeds.js b/src/stores/scoresaber/rankeds.js index baea1ae..a5cb5a9 100644 --- a/src/stores/scoresaber/rankeds.js +++ b/src/stores/scoresaber/rankeds.js @@ -1,7 +1,7 @@ -import {writable} from 'svelte/store' -import createRankedsService from '../../services/scoresaber/rankeds' -import {PRIORITY} from '../../network/queues/http-queue' -import eventBus from '../../utils/broadcast-channel-pubsub' +import { writable } from "svelte/store"; +import createRankedsService from "../../services/scoresaber/rankeds"; +import { PRIORITY } from "../../network/queues/http-queue"; +import eventBus from "../../utils/broadcast-channel-pubsub"; let store = null; let storeSubCount = 0; @@ -13,27 +13,33 @@ export default async (refreshOnCreate = false) => { let rankeds = refreshOnCreate ? {} : await rankedsService.get(); - const {subscribe: subscribeState, set} = writable(rankeds); + const { subscribe: subscribeState, set } = writable(rankeds); const get = () => rankeds; - const refresh = async (forceUpdate = false, priority = PRIORITY.BG_NORMAL) => { + const refresh = async ( + forceUpdate = false, + priority = PRIORITY.BG_NORMAL, + ) => { await rankedsService.refresh(forceUpdate, priority); - } + }; if (refreshOnCreate) await refresh(); rankeds = await rankedsService.get(); set(rankeds); - const rankedsChangedUnsubscribe = eventBus.on('rankeds-changed', ({allRankeds}) => { - if (allRankeds && Object.keys(allRankeds).length) set(allRankeds); - }) + const rankedsChangedUnsubscribe = eventBus.on( + "rankeds-changed", + ({ allRankeds }) => { + if (allRankeds && Object.keys(allRankeds).length) set(allRankeds); + }, + ); - const subscribe = fn => { + const subscribe = (fn) => { const stateUnsubscribe = subscribeState(fn); return () => { - storeSubCount --; + storeSubCount--; if (storeSubCount === 0) { store = null; @@ -43,14 +49,14 @@ export default async (refreshOnCreate = false) => { stateUnsubscribe(); rankedsChangedUnsubscribe(); } - } - } + }; + }; store = { subscribe, get, - refresh - } + refresh, + }; return store; -} \ No newline at end of file +}; diff --git a/src/svelte-utils/actions/hoverable.js b/src/svelte-utils/actions/hoverable.js index 4916d9b..88953cd 100644 --- a/src/svelte-utils/actions/hoverable.js +++ b/src/svelte-utils/actions/hoverable.js @@ -1,32 +1,49 @@ export function hoverable(node) { - function handleMouseover(event) { - node.classList.add('hovered'); + function handleMouseover(event) { + node.classList.add("hovered"); - const rect = node.getBoundingClientRect(); + const rect = node.getBoundingClientRect(); - node.dispatchEvent(new CustomEvent('hover', { - detail: { target: event.target, clientX: event.clientX, clientY: event.clientY, pageX: event.pageX, pageY: event.pageY, rect } - })); - } + node.dispatchEvent( + new CustomEvent("hover", { + detail: { + target: event.target, + clientX: event.clientX, + clientY: event.clientY, + pageX: event.pageX, + pageY: event.pageY, + rect, + }, + }), + ); + } - function handleMouseout(event) { - node.classList.remove('hovered'); + function handleMouseout(event) { + node.classList.remove("hovered"); - node.dispatchEvent(new CustomEvent('unhover', { - detail: { target: event.target, clientX: event.clientX, clientY: event.clientY, pageX: event.pageX, pageY: event.pageY } - })); - } + node.dispatchEvent( + new CustomEvent("unhover", { + detail: { + target: event.target, + clientX: event.clientX, + clientY: event.clientY, + pageX: event.pageX, + pageY: event.pageY, + }, + }), + ); + } - node.addEventListener('mouseover', handleMouseover); - node.addEventListener('mouseout', handleMouseout); + node.addEventListener("mouseover", handleMouseover); + node.addEventListener("mouseout", handleMouseout); - node.classList.add('hoverable'); + node.classList.add("hoverable"); - return { - destroy() { - node.removeEventListener('mouseover', handleMouseover); - node.removeEventListener('mouseout', handleMouseout); - node.classList.remove('hoverable'); - } - }; -} \ No newline at end of file + return { + destroy() { + node.removeEventListener("mouseover", handleMouseover); + node.removeEventListener("mouseout", handleMouseout); + node.classList.remove("hoverable"); + }, + }; +} diff --git a/src/svelte-utils/tweened.js b/src/svelte-utils/tweened.js index a5a8bd0..218c8ce 100644 --- a/src/svelte-utils/tweened.js +++ b/src/svelte-utils/tweened.js @@ -1,4 +1,5 @@ -import { tweened } from 'svelte/motion'; -import { cubicOut } from 'svelte/easing'; +import { tweened } from "svelte/motion"; +import { cubicOut } from "svelte/easing"; -export default (value, duration = 500) => tweened(value, {duration, easing: cubicOut}); \ No newline at end of file +export default (value, duration = 500) => + tweened(value, { duration, easing: cubicOut }); diff --git a/src/utils/accsaber/consts.js b/src/utils/accsaber/consts.js index a69e093..07cff6b 100644 --- a/src/utils/accsaber/consts.js +++ b/src/utils/accsaber/consts.js @@ -1,3 +1,3 @@ export const PLAYERS_PER_PAGE = 15; export const PLAYER_SCORES_PER_PAGE = 11; -export const LEADERBOARD_SCORES_PER_PAGE = 10; \ No newline at end of file +export const LEADERBOARD_SCORES_PER_PAGE = 10; diff --git a/src/utils/broadcast-channel-pubsub.js b/src/utils/broadcast-channel-pubsub.js index 346551a..26e9211 100644 --- a/src/utils/broadcast-channel-pubsub.js +++ b/src/utils/broadcast-channel-pubsub.js @@ -1,90 +1,113 @@ -import {BroadcastChannel, createLeaderElection} from 'broadcast-channel' -import {readable} from 'svelte/store' -import log from './logger' -import {uuid} from './uuid' +import { BroadcastChannel, createLeaderElection } from "broadcast-channel"; +import { readable } from "svelte/store"; +import log from "./logger"; +import { uuid } from "./uuid"; let bc; const createGlobalPubSub = () => { - const subscribers = {} + const subscribers = {}; - const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope; + const isWorker = + typeof WorkerGlobalScope !== "undefined" && + self instanceof WorkerGlobalScope; - const nodeId = uuid(); - log.info(`Create pub/sub channel for node ${nodeId} (${isWorker ? 'worker' : 'browser'})`, 'PubSub') + const nodeId = uuid(); + log.info( + `Create pub/sub channel for node ${nodeId} (${ + isWorker ? "worker" : "browser" + })`, + "PubSub", + ); - bc = new BroadcastChannel('global-pub-sub', {webWorkerSupport: true}); - const elector = createLeaderElection(bc); + bc = new BroadcastChannel("global-pub-sub", { webWorkerSupport: true }); + const elector = createLeaderElection(bc); - let isLeader = false; - const leaderStore = readable(isLeader, set => { - elector.awaitLeadership().then(() => { - isLeader = true; - set(isLeader); + let isLeader = false; + const leaderStore = readable(isLeader, (set) => { + elector.awaitLeadership().then(() => { + isLeader = true; + set(isLeader); - log.info(`Node ${nodeId} is a new leader`, 'PubSub') + log.info(`Node ${nodeId} is a new leader`, "PubSub"); - return () => {} - }); + return () => {}; }); + }); - const exists = eventName => Array.isArray(subscribers[eventName]); + const exists = (eventName) => Array.isArray(subscribers[eventName]); - const notify = (eventName, value, isLocal = true) => { - if (!exists(eventName)) return; + const notify = (eventName, value, isLocal = true) => { + if (!exists(eventName)) return; - subscribers[eventName].forEach(handler => handler(value, isLocal, eventName)); - } + subscribers[eventName].forEach((handler) => + handler(value, isLocal, eventName), + ); + }; - const unsubscribe = (eventName, handler) => { - if (!exists(eventName)) return; + const unsubscribe = (eventName, handler) => { + if (!exists(eventName)) return; - subscribers[eventName] = subscribers[eventName].filter(h => h !== handler); - } + subscribers[eventName] = subscribers[eventName].filter( + (h) => h !== handler, + ); + }; - const publish = (eventName, value) => { - notify(eventName, value); + const publish = (eventName, value) => { + notify(eventName, value); - bc.postMessage({eventName, nodeId, value}) - } + bc.postMessage({ eventName, nodeId, value }); + }; - bc.onmessage = ({eventName, nodeId: eventNodeId, value}) => notify(eventName, value, eventNodeId === nodeId); + bc.onmessage = ({ eventName, nodeId: eventNodeId, value }) => + notify(eventName, value, eventNodeId === nodeId); - const removeNode = async () => { - log.info(`Node ${nodeId} is about to be removed`, 'PubSub'); + const removeNode = async () => { + log.info(`Node ${nodeId} is about to be removed`, "PubSub"); - publish('node-removed', nodeId); - } + publish("node-removed", nodeId); + }; - // add close handler (also prevents back-forward cache) - if (!(typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)) { - window.addEventListener('beforeunload', () => removeNode(), {capture: true}); - } + // add close handler (also prevents back-forward cache) + if ( + !( + typeof WorkerGlobalScope !== "undefined" && + self instanceof WorkerGlobalScope + ) + ) { + window.addEventListener("beforeunload", () => removeNode(), { + capture: true, + }); + } - publish('node-added', nodeId) - log.info(`Node ${nodeId} has been created`, 'PubSub') + publish("node-added", nodeId); + log.info(`Node ${nodeId} has been created`, "PubSub"); - return { - on(eventName, handler) { - if (!exists(eventName)) subscribers[eventName] = []; + return { + on(eventName, handler) { + if (!exists(eventName)) subscribers[eventName] = []; - // workaround - have no idea why some handlers are registered multiple times - if (subscribers[eventName].find(h => h === handler)) return; + // workaround - have no idea why some handlers are registered multiple times + if (subscribers[eventName].find((h) => h === handler)) return; - subscribers[eventName].push(handler); + subscribers[eventName].push(handler); - return () => { - unsubscribe(eventName, handler); - } - }, - unsubscribe, - publish, - leaderStore, - isLeader() {return isLeader}, - getNodeId() {return nodeId}, - } -} + return () => { + unsubscribe(eventName, handler); + }; + }, + unsubscribe, + publish, + leaderStore, + isLeader() { + return isLeader; + }, + getNodeId() { + return nodeId; + }, + }; +}; const pubSub = createGlobalPubSub(); -export default pubSub; \ No newline at end of file +export default pubSub; diff --git a/src/utils/browser.js b/src/utils/browser.js index 3b841f5..c70d165 100644 --- a/src/utils/browser.js +++ b/src/utils/browser.js @@ -1,15 +1,15 @@ -import {opt} from './js' +import { opt } from "./js"; export function scrollToTargetAdjusted(target, offset = 0) { if (!target) return; - const elementPosition = opt(target.getBoundingClientRect(), 'top'); + const elementPosition = opt(target.getBoundingClientRect(), "top"); if (!elementPosition) return; - const offsetPosition = elementPosition - offset + window.pageYOffset + const offsetPosition = elementPosition - offset + window.pageYOffset; window.scrollTo({ top: offsetPosition, behavior: "smooth", }); -} \ No newline at end of file +} diff --git a/src/utils/cf-email-decrypt.js b/src/utils/cf-email-decrypt.js index d4d0833..7b21e11 100644 --- a/src/utils/cf-email-decrypt.js +++ b/src/utils/cf-email-decrypt.js @@ -1 +1,76 @@ -export default function (document) {"use strict";function e(e){try{if("undefined"==typeof console)return;"error"in console?console.error(e):console.log(e)}catch(e){}}function t(e){return d.innerHTML='',d.childNodes[0].getAttribute("href")||""}function r(e,t){var r=e.substr(t,2);return parseInt(r,16)}function n(n,c){for(var o="",a=r(n,c),i=c+2;i