diff --git a/src/GameFetcher.ts b/src/GameFetcher.ts new file mode 100644 index 0000000..00ee129 --- /dev/null +++ b/src/GameFetcher.ts @@ -0,0 +1,211 @@ +import { Mutex } from "../deps.ts"; +import { State } from "./state.ts"; +import { + getBankaraBattleHistories, + getBattleDetail, + getCoopDetail, + getCoopHistories, +} from "./splatnet3.ts"; +import { + BattleListNode, + ChallengeProgress, + CoopInfo, + CoopListNode, + Game, + HistoryGroups, + VsInfo, +} from "./types.ts"; +import { Cache, MemoryCache } from "./cache.ts"; +import { gameId } from "./utils.ts"; + +/** + * Fetch game and cache it. It also fetches bankara match challenge info. + */ +export class GameFetcher { + state: State; + cache: Cache; + lock: Record = {}; + bankaraLock = new Mutex(); + bankaraHistory?: HistoryGroups["nodes"]; + coopLock = new Mutex(); + coopHistory?: HistoryGroups["nodes"]; + + constructor( + { cache = new MemoryCache(), state }: { state: State; cache?: Cache }, + ) { + this.state = state; + this.cache = cache; + } + private getLock(id: string): Mutex { + let cur = this.lock[id]; + + if (!cur) { + cur = new Mutex(); + this.lock[id] = cur; + } + + return cur; + } + + getBankaraHistory() { + return this.bankaraLock.use(async () => { + if (this.bankaraHistory) { + return this.bankaraHistory; + } + + const { bankaraBattleHistories: { historyGroups } } = + await getBankaraBattleHistories( + this.state, + ); + + this.bankaraHistory = historyGroups.nodes; + + return this.bankaraHistory; + }); + } + getCoopHistory() { + return this.coopLock.use(async () => { + if (this.coopHistory) { + return this.coopHistory; + } + + const { coopResult: { historyGroups } } = await getCoopHistories( + this.state, + ); + + this.coopHistory = historyGroups.nodes; + + return this.coopHistory; + }); + } + async getCoopMetaById(id: string): Promise> { + const coopHistory = await this.getCoopHistory(); + const group = coopHistory.find((i) => + i.historyDetails.nodes.some((i) => i.id === id) + ); + + if (!group) { + return { + type: "CoopInfo", + listNode: null, + }; + } + + const listNode = group.historyDetails.nodes.find((i) => i.id === id) ?? + null; + + return { + type: "CoopInfo", + listNode, + }; + } + async getBattleMetaById(id: string): Promise> { + const gid = await gameId(id); + const bankaraHistory = await this.getBankaraHistory(); + const gameIdMap = new Map(); + + for (const i of bankaraHistory) { + for (const j of i.historyDetails.nodes) { + gameIdMap.set(j, await gameId(j.id)); + } + } + + const group = bankaraHistory.find((i) => + i.historyDetails.nodes.some((i) => gameIdMap.get(i) === gid) + ); + + if (!group) { + return { + type: "VsInfo", + challengeProgress: null, + bankaraMatchChallenge: null, + listNode: null, + }; + } + + const { bankaraMatchChallenge } = group; + const listNode = + group.historyDetails.nodes.find((i) => gameIdMap.get(i) === gid) ?? + null; + const index = group.historyDetails.nodes.indexOf(listNode!); + + let challengeProgress: null | ChallengeProgress = null; + if (bankaraMatchChallenge) { + const pastBattles = group.historyDetails.nodes.slice(0, index); + const { winCount, loseCount } = bankaraMatchChallenge; + challengeProgress = { + index, + winCount: winCount - + pastBattles.filter((i) => i.judgement == "WIN").length, + loseCount: loseCount - + pastBattles.filter((i) => + ["LOSE", "DEEMED_LOSE"].includes(i.judgement) + ).length, + }; + } + + return { + type: "VsInfo", + bankaraMatchChallenge, + listNode, + challengeProgress, + }; + } + cacheDetail( + id: string, + getter: () => Promise, + ): Promise { + const lock = this.getLock(id); + + return lock.use(async () => { + const cached = await this.cache.read(id); + if (cached) { + return cached; + } + + const detail = await getter(); + + await this.cache.write(id, detail); + + return detail; + }); + } + fetch(type: Game["type"], id: string): Promise { + switch (type) { + case "VsInfo": + return this.fetchBattle(id); + case "CoopInfo": + return this.fetchCoop(id); + default: + throw new Error(`Unknown game type: ${type}`); + } + } + async fetchBattle(id: string): Promise { + const detail = await this.cacheDetail( + id, + () => getBattleDetail(this.state, id).then((r) => r.vsHistoryDetail), + ); + const metadata = await this.getBattleMetaById(id); + + const game: VsInfo = { + ...metadata, + detail, + }; + + return game; + } + async fetchCoop(id: string): Promise { + const detail = await this.cacheDetail( + id, + () => getCoopDetail(this.state, id).then((r) => r.coopHistoryDetail), + ); + const metadata = await this.getCoopMetaById(id); + + const game: CoopInfo = { + ...metadata, + detail, + }; + + return game; + } +} diff --git a/src/app.ts b/src/app.ts index 5bf40c2..1d0d228 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,41 +1,24 @@ import { getBulletToken, getGToken, loginManually } from "./iksm.ts"; -import { MultiProgressBar, Mutex } from "../deps.ts"; +import { MultiProgressBar } from "../deps.ts"; import { DEFAULT_STATE, FileStateBackend, State, StateBackend, } from "./state.ts"; -import { - getBankaraBattleHistories, - getBattleDetail, - getBattleList, - getCoopDetail, - getCoopHistories, - isTokenExpired, -} from "./splatnet3.ts"; -import { - BattleListNode, - BattleListType, - ChallengeProgress, - CoopInfo, - CoopListNode, - Game, - GameExporter, - HistoryGroups, - VsInfo, -} from "./types.ts"; -import { Cache, FileCache, MemoryCache } from "./cache.ts"; +import { getBattleList, isTokenExpired } from "./splatnet3.ts"; +import { BattleListType, Game, GameExporter } from "./types.ts"; +import { Cache, FileCache } from "./cache.ts"; import { StatInkExporter } from "./exporters/stat.ink.ts"; import { FileExporter } from "./exporters/file.ts"; import { delay, - gameId, readline, RecoverableError, retryRecoverableError, showError, } from "./utils.ts"; +import { GameFetcher } from "./GameFetcher.ts"; export type Opts = { profilePath: string; @@ -54,198 +37,6 @@ export const DEFAULT_OPTS: Opts = { monitor: false, }; -/** - * Fetch game and cache it. - */ -class GameFetcher { - state: State; - cache: Cache; - lock: Record = {}; - bankaraLock = new Mutex(); - bankaraHistory?: HistoryGroups["nodes"]; - coopLock = new Mutex(); - coopHistory?: HistoryGroups["nodes"]; - - constructor( - { cache = new MemoryCache(), state }: { state: State; cache?: Cache }, - ) { - this.state = state; - this.cache = cache; - } - private getLock(id: string): Mutex { - let cur = this.lock[id]; - - if (!cur) { - cur = new Mutex(); - this.lock[id] = cur; - } - - return cur; - } - - getBankaraHistory() { - return this.bankaraLock.use(async () => { - if (this.bankaraHistory) { - return this.bankaraHistory; - } - - const { bankaraBattleHistories: { historyGroups } } = - await getBankaraBattleHistories( - this.state, - ); - - this.bankaraHistory = historyGroups.nodes; - - return this.bankaraHistory; - }); - } - getCoopHistory() { - return this.coopLock.use(async () => { - if (this.coopHistory) { - return this.coopHistory; - } - - const { coopResult: { historyGroups } } = await getCoopHistories( - this.state, - ); - - this.coopHistory = historyGroups.nodes; - - return this.coopHistory; - }); - } - async getCoopMetaById(id: string): Promise> { - const coopHistory = await this.getCoopHistory(); - const group = coopHistory.find((i) => - i.historyDetails.nodes.some((i) => i.id === id) - ); - - if (!group) { - return { - type: "CoopInfo", - listNode: null, - }; - } - - const listNode = group.historyDetails.nodes.find((i) => i.id === id) ?? - null; - - return { - type: "CoopInfo", - listNode, - }; - } - async getBattleMetaById(id: string): Promise> { - const gid = await gameId(id); - const bankaraHistory = await this.getBankaraHistory(); - const gameIdMap = new Map(); - - for (const i of bankaraHistory) { - for (const j of i.historyDetails.nodes) { - gameIdMap.set(j, await gameId(j.id)); - } - } - - const group = bankaraHistory.find((i) => - i.historyDetails.nodes.some((i) => gameIdMap.get(i) === gid) - ); - - if (!group) { - return { - type: "VsInfo", - challengeProgress: null, - bankaraMatchChallenge: null, - listNode: null, - }; - } - - const { bankaraMatchChallenge } = group; - const listNode = - group.historyDetails.nodes.find((i) => gameIdMap.get(i) === gid) ?? - null; - const index = group.historyDetails.nodes.indexOf(listNode!); - - let challengeProgress: null | ChallengeProgress = null; - if (bankaraMatchChallenge) { - const pastBattles = group.historyDetails.nodes.slice(0, index); - const { winCount, loseCount } = bankaraMatchChallenge; - challengeProgress = { - index, - winCount: winCount - - pastBattles.filter((i) => i.judgement == "WIN").length, - loseCount: loseCount - - pastBattles.filter((i) => - ["LOSE", "DEEMED_LOSE"].includes(i.judgement) - ).length, - }; - } - - return { - type: "VsInfo", - bankaraMatchChallenge, - listNode, - challengeProgress, - }; - } - cacheDetail( - id: string, - getter: () => Promise, - ): Promise { - const lock = this.getLock(id); - - return lock.use(async () => { - const cached = await this.cache.read(id); - if (cached) { - return cached; - } - - const detail = await getter(); - - await this.cache.write(id, detail); - - return detail; - }); - } - fetch(type: Game["type"], id: string): Promise { - switch (type) { - case "VsInfo": - return this.fetchBattle(id); - case "CoopInfo": - return this.fetchCoop(id); - default: - throw new Error(`Unknown game type: ${type}`); - } - } - async fetchBattle(id: string): Promise { - const detail = await this.cacheDetail( - id, - () => getBattleDetail(this.state, id).then((r) => r.vsHistoryDetail), - ); - const metadata = await this.getBattleMetaById(id); - - const game: VsInfo = { - ...metadata, - detail, - }; - - return game; - } - async fetchCoop(id: string): Promise { - const detail = await this.cacheDetail( - id, - () => getCoopDetail(this.state, id).then((r) => r.coopHistoryDetail), - ); - const metadata = await this.getCoopMetaById(id); - - const game: CoopInfo = { - ...metadata, - detail, - }; - - return game; - } -} - type Progress = { currentUrl?: string; current: number;