From 7ccf28d6d1cf2338080bc5c9e7a9c83cf7a1a388 Mon Sep 17 00:00:00 2001 From: dtinth on MBP M1 Date: Sat, 7 Jan 2023 03:16:18 +0700 Subject: [PATCH 1/2] Clean up online ranking code --- bemuse/src/app/ui/RankingContainer.tsx | 79 +------- bemuse/src/online/index.spec.js | 114 ++---------- bemuse/src/online/index.ts | 243 +------------------------ bemuse/src/test/index.js | 4 +- 4 files changed, 30 insertions(+), 410 deletions(-) diff --git a/bemuse/src/app/ui/RankingContainer.tsx b/bemuse/src/app/ui/RankingContainer.tsx index 6a245b778..ed7bdb98c 100644 --- a/bemuse/src/app/ui/RankingContainer.tsx +++ b/bemuse/src/app/ui/RankingContainer.tsx @@ -1,20 +1,18 @@ -import { RankingState, RankingStream } from 'bemuse/online' +import { RankingState } from 'bemuse/online' import { useCurrentUser, useLeaderboardQuery, usePersonalRankingEntryQuery, useRecordSubmissionMutation, } from 'bemuse/online/hooks' -import React, { useContext, useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef } from 'react' +import { Operation, completed, error, loading } from 'bemuse/online/operations' +import { ScoreCount } from 'bemuse/rules/accuracy' import { MappingMode } from 'bemuse/rules/mapping-mode' -import { OnlineContext } from 'bemuse/online/instance' +import { UseMutationResult, UseQueryResult } from 'react-query' import Ranking from './Ranking' import { Result } from './ResultScene' -import { ScoreCount } from 'bemuse/rules/accuracy' -import { isQueryFlagEnabled } from 'bemuse/flags' -import { UseMutationResult, UseQueryResult } from 'react-query' -import { Operation, completed, error, loading } from 'bemuse/online/operations' export interface RankingContainerProps { chart: { md5: string } @@ -22,68 +20,7 @@ export interface RankingContainerProps { result?: Result } -/** @deprecated */ -export const OldRankingContainer = ({ - chart, - playMode, - result, -}: RankingContainerProps) => { - const online = useContext(OnlineContext) - const [state, setState] = useState({ - data: null, - meta: { - scoreboard: { status: 'loading' }, - submission: { status: 'loading' }, - }, - }) - const model = useRef(null) - useEffect(() => { - const onStoreTrigger = (newState: RankingState) => - setState(() => ({ ...newState })) - const params = { - md5: chart.md5, - playMode: playMode, - ...(result - ? { - score: result.score, - combo: result.maxCombo, - total: result.totalCombo, - count: [ - result['1'], - result['2'], - result['3'], - result['4'], - result.missed, - ] as ScoreCount, - log: result.log, - } - : {}), - } - model.current = online.Ranking(params) - const subscription = model.current.state川.subscribe(onStoreTrigger) - return () => { - subscription.unsubscribe() - } - }, []) - - const onReloadScoreboardRequest = () => { - model.current?.reloadScoreboard() - } - - const onResubmitScoreRequest = () => { - model.current?.resubmit() - } - - return ( - - ) -} - -export const NewRankingContainer = ({ +export const RankingContainer = ({ chart, playMode, result, @@ -154,6 +91,4 @@ function operationFromResult( return completed(result.data!) } -export default (isQueryFlagEnabled('old-ranking') - ? OldRankingContainer - : NewRankingContainer) as FC +export default RankingContainer as FC diff --git a/bemuse/src/online/index.spec.js b/bemuse/src/online/index.spec.js index 421ef39e6..5f2ad4d35 100644 --- a/bemuse/src/online/index.spec.js +++ b/bemuse/src/online/index.spec.js @@ -55,6 +55,7 @@ function tests(onlineServiceOptions) { this.timeout(20000) describe('signup', function () { + /** @type {ReturnType} */ let online let info before(function () { @@ -75,6 +76,7 @@ function tests(onlineServiceOptions) { }) describe('initially', function () { + /** @type {ReturnType} */ let online before(async () => { await createOnline().logOut() @@ -82,41 +84,34 @@ function tests(onlineServiceOptions) { beforeEach(async () => { online = createOnline() }) - describe('user川', function () { - it('should be null', function (done) { - online.user川.pipe(first()).subscribe((user) => { - assert(user === null) - done() - }) + describe('getCurrentUser', function () { + it('should return null', function () { + assert.equal(online.getCurrentUser(), null) }) }) }) describe('when signed up', function () { + /** @type {ReturnType} */ let online before(function () { online = createOnline() }) - describe('user川', function () { + describe('getCurrentUser', function () { it('should change to signed-up user, and also start with it', async function () { const info = createAccountInfo() - await online.signUp(info) + const user = online.getCurrentUser() + assert.equal(user.username, info.username) - const user = await firstValueFrom( - online.user川.pipe(filter((u) => !!u)) - ) - expect(user.username).to.equal(info.username) - - const firstUser = await firstValueFrom( - createOnline().user川.pipe(filter((u) => !!u)) - ) - expect(firstUser.username).to.equal(info.username) + const anotherUser = createOnline().getCurrentUser() + assert.equal(anotherUser.username, info.username) }) }) }) describe('with an active user', function () { + /** @type {ReturnType} */ let online const info = createAccountInfo() before(function () { @@ -127,19 +122,15 @@ function tests(onlineServiceOptions) { return online.logIn(info) }) describe('when log out', function () { - it('should change user川 back to null', async function () { - online.logOut() - - const user = await firstValueFrom( - online.user川.pipe(filter((u) => !u)).pipe(first()) - ) - - assert(user === null) + it('should change getCurrentUser result back to null', async function () { + await online.logOut() + assert.equal(online.getCurrentUser(), null) }) }) }) describe('submitting high scores', function () { + /** @type {ReturnType} */ let online before(function () { online = createOnline() @@ -243,6 +234,7 @@ function tests(onlineServiceOptions) { }) describe('the scoreboard', function () { + /** @type {ReturnType} */ let online before(function () { online = createOnline() @@ -295,78 +287,6 @@ function tests(onlineServiceOptions) { step('log out...', function () { return online.logOut() }) - - let ranking - step('subscribe to scoreboard...', function () { - ranking = online.Ranking({ - md5: prefix + 'song1', - playMode: 'BM', - score: 111111, - combo: 123, - total: 456, - count: [0, 123, 0, 0, 333], - log: '', - }) - }) - - function when(predicate) { - return firstValueFrom(ranking.state川.pipe(filter(predicate))) - } - - step('should have scoreboard loading status', function () { - return when((state) => state.meta.scoreboard.status === 'loading') - }) - step('no new score should be submitted', function () { - return when( - (state) => - state.meta.scoreboard.status === 'completed' && - state.meta.submission.status === 'unauthenticated' - ).then((state) => { - expect(state.data).to.have.length(2) - }) - }) - step('sign up user3...', function () { - return online.signUp(user3) - }) - step('should start sending score', function () { - return when((state) => state.meta.submission.status === 'loading') - }) - step('should finish sending score', function () { - return when( - (state) => state.meta.submission.status === 'completed' - ).then((state) => { - expect(state.meta.submission.value.rank).to.equal(3) - }) - }) - step('should start loading scoreboard', function () { - return when((state) => state.meta.scoreboard.status === 'loading') - }) - step('should finish reloading scoreboard', function () { - return when( - (state) => state.meta.scoreboard.status === 'completed' - ).then((state) => { - expect(state.data).to.have.length(3) - }) - }) - step('resubscribe with read only', function () { - ranking = online.Ranking({ - md5: prefix + 'song1', - playMode: 'BM', - }) - }) - step('should not submit new score', function () { - return when( - (state) => - state.meta.scoreboard.status === 'completed' && - state.meta.submission.status === 'completed' - ).then(function (state) { - expect(state.data).to.have.length(3) - expect(state.meta.submission.value.playCount).to.equal(1) - }) - }) - after(function () { - online.logOut() - }) }) }) }) diff --git a/bemuse/src/online/index.ts b/bemuse/src/online/index.ts index 5632d70b1..dcff1700e 100644 --- a/bemuse/src/online/index.ts +++ b/bemuse/src/online/index.ts @@ -1,45 +1,10 @@ -import { - Action, - DataStore, - initialState, - put, - putMultiple, - store川, -} from './data-store' -import { - INITIAL_OPERATION_STATE, - Operation, - completed, - operation川FromPromise, -} from './operations' -import { - Observable, - ObservableInput, - Subject, - asapScheduler, - bufferTime, - combineLatest, - concatMap, - distinctUntilChanged, - from, - map, - merge, - of, - scan, - scheduled, - shareReplay, - startWith, - switchMap, -} from 'rxjs' -import { RecordLevel, fromObject } from './level' +import { RecordLevel } from './level' +import { Operation } from './operations' -import Immutable from 'immutable' -import { ScoreCount } from 'bemuse/rules/accuracy' -import _ from 'lodash' -import id from './id' import { queryClient } from 'bemuse/react-query' -import { rootQueryKey } from './queryKeys' +import { ScoreCount } from 'bemuse/rules/accuracy' import { BatchedFetcher } from './BatchedFetcher' +import { rootQueryKey } from './queryKeys' export interface SignUpInfo { username: string @@ -70,11 +35,6 @@ export interface ScoreBase { export type ScoreInfo = ScoreBase & RecordLevel -export type RankingInfo = Partial & RecordLevel - -const scoreInfoGuard = (data: ScoreInfo | RankingInfo): data is ScoreInfo => - !!data.score - export interface ScoreboardDataEntry { rank?: number score: number @@ -119,50 +79,21 @@ export interface InternetRankingService { ): Promise } -/** @deprecated */ -export interface RankingStream { - /** @deprecated */ - state川: Observable - - /** @deprecated */ - resubmit: () => void - - /** @deprecated */ - reloadScoreboard: () => void -} - export class Online { constructor(private readonly service: InternetRankingService) {} - /** @deprecated */ - private user口 = new Subject() - - /** @deprecated */ - private seen口 = new Subject() - - /** @deprecated */ - private submitted口 = new Subject() - - /** @deprecated - Use getCurrentUser() instead */ - user川 = this.user口 - .pipe(startWith(null)) - .pipe(shareReplay(1)) - .pipe(map((user) => user || this.service.getCurrentUser())) - getCurrentUser() { return this.service.getCurrentUser() } async signUp(options: SignUpInfo) { const user = await this.service.signUp(options) - this.user口.next(user) queryClient.invalidateQueries({ queryKey: rootQueryKey }) return user } async logIn(options: LogInInfo) { const user = await this.service.logIn(options) - this.user口.next(user) queryClient.invalidateQueries({ queryKey: rootQueryKey }) return user } @@ -184,7 +115,6 @@ export class Online { async logOut(): Promise { await this.service.logOut() - this.user口.next(null) queryClient.invalidateQueries({ queryKey: rootQueryKey }) } @@ -193,7 +123,6 @@ export class Online { throw new Error('Unauthenticated.') } const record = await this.service.submitScore(info) - this.submitted口.next(record) return record } @@ -205,170 +134,6 @@ export class Online { if (!this.service.getCurrentUser()) return null return this.service.retrieveRecord(level) } - - private allSeen川 = this.allSeen川ForJustSeen川(this.seen口) - - private allSeen川ForJustSeen川( - justSeen川: Observable - ): Observable { - return justSeen川 - .pipe(bufferTime(138)) - .pipe( - scan( - (map, seen) => - map.merge(Immutable.Map(_.zipObject(seen.map(id), seen))), - Immutable.Map() - ) - ) - .pipe(map((map) => map.valueSeq())) - .pipe(distinctUntilChanged(Immutable.is)) - .pipe(map((seq) => seq.toArray())) - } - - private fetchRecords = async ( - levels: readonly RecordLevel[], - user: UserInfo | null, - seen: Set - ): Promise> => { - const levelsToFetch = levels.filter((level) => !seen.has(id(level))) - for (const level of levelsToFetch) { - seen.add(id(level)) - } - const results = - user && levelsToFetch.length > 0 - ? await this.service.retrieveMultipleRecords(levelsToFetch) - : [] - try { - const loadedRecords = _.zipObject(results.map(id), results.map(completed)) - const nullResults = _.zipObject( - levelsToFetch.map(id), - levelsToFetch.map(() => completed(null)) - ) - const transitions = _.defaults(loadedRecords, nullResults) - return putMultiple(transitions) - } catch (e: unknown) { - console.error('Cannot fetch levels:', e) - return putMultiple({}) - } - } - - private records川ForUser = ( - user: UserInfo | null - ): Observable> => { - const seen = new Set() - - const action川 = merge( - this.allSeen川.pipe( - concatMap((x) => from(this.fetchRecords(x, user, seen))) - ), - this.submitted口.pipe(map((record) => put(id(record), completed(record)))) - ) - return store川(action川) - } - - /** @deprecated */ - records川 = this.user川 - .pipe(switchMap(this.records川ForUser)) - .pipe(startWith(initialState())) - .pipe(shareReplay(1)) - - dispose() {} - - /** @deprecated */ - Ranking(data: RankingInfo): RankingStream { - const level: RecordLevel = fromObject(data) - const retrySelf口 = new Subject() - const retryScoreboard口 = new Subject() - - const self川 = this.self川ForUser(retrySelf口, data) - const scoreboard川 = self川 - .pipe( - switchMap(() => this.getScoreboardState川(retryScoreboard口, level)) - ) - .pipe(shareReplay(1)) - const state川 = combineLatest({ - self: self川, - scoreboard: scoreboard川, - }).pipe(map(this.conformState)) - return { - state川, - resubmit: () => retrySelf口.next(), - reloadScoreboard: () => retryScoreboard口.next(), - } - } - - // Make the state conform the old API. We should remove this in the future. - private conformState = ({ - self, - scoreboard, - }: { - self: SubmissionOperation - scoreboard: Operation<{ data: ScoreboardDataEntry[] }> - }): RankingState => ({ - data: - scoreboard.status === 'completed' ? scoreboard.value?.data ?? null : null, - meta: { - scoreboard: _.omit(scoreboard, 'value') as Operation, - submission: { ...self } as Operation, - }, - }) - - private self川ForUser = ( - retrySelfBus: Observable, - data: ScoreInfo | RankingInfo - ): Observable => - this.user川 - .pipe( - switchMap((user) => { - if (!user) { - return this.unauthenticatedRankingModel() - } - if (scoreInfoGuard(data)) { - return this.submissionModel(retrySelfBus, data) - } - return this.viewRecordModel(retrySelfBus, data) - }) - ) - .pipe(startWith(INITIAL_OPERATION_STATE)) - .pipe(shareReplay(1)) - - private unauthenticatedRankingModel = (): Observable => - of({ - status: 'unauthenticated', - error: null, - record: null, - }) - - private submissionModel = ( - retrySelfBus: Observable, - data: ScoreInfo - ): Observable => - merge(this.asap川([[]]), retrySelfBus).pipe( - switchMap(() => operation川FromPromise(this.submitScore(data))) - ) - - private viewRecordModel = ( - retrySelfBus: Observable, - data: RankingInfo - ): Observable => - merge(this.asap川([[]]), retrySelfBus).pipe( - switchMap(() => operation川FromPromise(this.service.retrieveRecord(data))) - ) - - private getScoreboardState川 = ( - retryScoreboardBus: Observable, - level: RecordLevel - ): Observable> => - merge(this.asap川([[]]), retryScoreboardBus).pipe( - switchMap(() => operation川FromPromise(this.scoreboard(level))) - ) - - private asap川 = (input: ObservableInput) => - scheduled(input, asapScheduler) - - seen(level: RecordLevel) { - return this.seen口.next(level) - } } export default Online diff --git a/bemuse/src/test/index.js b/bemuse/src/test/index.js index b0fe7cb70..4ebff83e3 100644 --- a/bemuse/src/test/index.js +++ b/bemuse/src/test/index.js @@ -1,8 +1,8 @@ // This file boots up Mocha // import 'script-loader!mocha/mocha.js' -import 'style-loader!mocha/mocha.css' -import 'style-loader!./support/mocha-overrides.css' +import 'mocha/mocha.css' +import './support/mocha-overrides.css' import loadSpecs from './loadSpecs' import prepareTestEnvironment from './prepareTestEnvironment' From fd18641ac82e898429a6fea8dc88ad63099cec0f Mon Sep 17 00:00:00 2001 From: dtinth on MBP M1 Date: Sat, 7 Jan 2023 03:18:55 +0700 Subject: [PATCH 2/2] Bye bye baconjs --- bemuse/package.json | 1 - bemuse/src/flux/index.js | 90 ---------------------------------- bemuse/src/omni-input/index.ts | 2 +- 3 files changed, 1 insertion(+), 92 deletions(-) delete mode 100644 bemuse/src/flux/index.js diff --git a/bemuse/package.json b/bemuse/package.json index 2b36b9117..9d6769e9f 100644 --- a/bemuse/package.json +++ b/bemuse/package.json @@ -131,7 +131,6 @@ "@reduxjs/toolkit": "^1.9.1", "auth0-js": "^9.8.0", "axios": "^1.1.3", - "baconjs": "^0.7.95", "bemuse-indexer": "^51.0.2", "bemuse-notechart": "^50.1.2", "bemuse-types": "^50.0.2", diff --git a/bemuse/src/flux/index.js b/bemuse/src/flux/index.js deleted file mode 100644 index 790721f7a..000000000 --- a/bemuse/src/flux/index.js +++ /dev/null @@ -1,90 +0,0 @@ -import Bacon from 'baconjs' -import React from 'react' - -export { Bacon } - -let lock = false - -export function Action(transform = (x) => x) { - const bus = new Bacon.Bus() - const action = function () { - if (lock) { - throw new Error( - 'An action should not fire another action! (' + lock.stack + ')' - ) - } - try { - const payload = transform.apply(null, arguments) - lock = new Error('Previous lock:') - bus.push(payload) - } finally { - lock = false - } - } - action.bus = bus - action.debug = function (prefix) { - bus.map((value) => [prefix, value]).log() - return action - } - return action -} - -export function Store(store, options = {}) { - const lazy = !!options.lazy - store = toProperty(store) - store.get = () => { - let data - const unsubscribe = store.onValue((_data) => (data = _data)) - setTimeout(unsubscribe) - return data - } - if (!lazy) { - store.onValue(() => {}) - } - return store -} - -function toProperty(store) { - if (store instanceof Bacon.Property) { - return store - } else if (store instanceof Bacon.EventStream) { - throw new Error('Please convert Bacon.EventStream to Bacon.Property first.') - } else if (store && typeof store === 'object') { - return Bacon.combineTemplate(store) - } else { - throw new Error('Expected a Bacon.Property or a template.') - } -} - -export const connect = (props川) => (Component) => { - const propsProperty = toProperty(props川) - return class extends React.Component { - constructor(props) { - super(props) - this._unsubscribe = propsProperty.onValue(this.handleValue) - let initialValue - const initialUnsubscribe = propsProperty.onValue( - (value) => (initialValue = value) - ) - initialUnsubscribe() - this.state = { value: initialValue } - } - - componentWillUnmount() { - this._mounted = false - if (this._unsubscribe) this._unsubscribe() - } - - componentDidMount() { - this._mounted = true - } - - handleValue = (value) => { - if (this._mounted) this.setState({ value }) - } - - render() { - return - } - } -} diff --git a/bemuse/src/omni-input/index.ts b/bemuse/src/omni-input/index.ts index 05c44e6e8..a1ad31875 100644 --- a/bemuse/src/omni-input/index.ts +++ b/bemuse/src/omni-input/index.ts @@ -190,7 +190,7 @@ export class OmniInput { } } -// Public: Returns a Bacon EventStream of keys pressed. +// Public: Returns an RxJS Observable of keys pressed. // export function key川( input = new OmniInput(),