the browser-facing portion of osu!
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
2// See the LICENCE file in the repository root for full licence text.
3
4import DispatcherAction from 'actions/dispatcher-action';
5import { UserLoginAction } from 'actions/user-login-actions';
6import { dispatchListener } from 'app-dispatcher';
7import ResultSet from 'beatmaps/result-set';
8import SearchResults from 'beatmaps/search-results';
9import { BeatmapsetSearchFilters } from 'beatmapset-search-filters';
10import DispatchListener from 'dispatch-listener';
11import BeatmapsetExtendedJson from 'interfaces/beatmapset-extended-json';
12import { route } from 'laroute';
13import { action, makeObservable, observable, runInAction } from 'mobx';
14import { BeatmapsetStore } from 'stores/beatmapset-store';
15
16export interface SearchResponse {
17 beatmapsets: BeatmapsetExtendedJson[];
18 cursor_string: string | null;
19 error?: string;
20 recommended_difficulty: number;
21 total: number;
22}
23
24@dispatchListener
25export class BeatmapsetSearch implements DispatchListener {
26 @observable readonly recommendedDifficulties = new Map<string|null, number>();
27 @observable readonly resultSets = new Map<string, ResultSet>();
28
29 private xhr?: JQueryXHR;
30
31 constructor(private readonly beatmapsetStore: BeatmapsetStore) {
32 makeObservable(this);
33 }
34
35 cancel() {
36 if (this.xhr) {
37 this.xhr.abort();
38 }
39 }
40
41 @action
42 get(filters: BeatmapsetSearchFilters, from = 0): PromiseLike<SearchResults> {
43 if (from < 0) {
44 throw Error('from must be > 0');
45 }
46
47 const key = filters.toKeyString();
48 const resultSet = this.getOrCreate(key);
49 const sufficient = (from > 0 && from < resultSet.beatmapsetIds.size) || (from === 0 && !resultSet.isExpired);
50 if (sufficient) {
51 return Promise.resolve(resultSet);
52 }
53
54 return this.fetch(filters, from).then((data) => {
55 if (data != null) {
56 runInAction(() => {
57 if (from === 0) {
58 resultSet.reset();
59 }
60
61 this.updateBeatmapsetStore(data);
62 resultSet.append(data);
63 this.recommendedDifficulties.set(filters.mode, data.recommended_difficulty);
64 });
65 }
66
67 return resultSet;
68 });
69 }
70
71 getResultSet(filters: BeatmapsetSearchFilters) {
72 const key = filters.toKeyString();
73
74 return this.getOrCreate(key);
75 }
76
77 handleDispatchAction(dispatcherAction: DispatcherAction) {
78 if (dispatcherAction instanceof UserLoginAction) {
79 this.clear();
80 }
81 }
82
83 @action
84 initialize(filters: BeatmapsetSearchFilters, data: SearchResponse) {
85 this.updateBeatmapsetStore(data);
86
87 const key = filters.toKeyString();
88 const resultSet = this.getOrCreate(key);
89 // skip if already tracking.
90 if (resultSet.fetchedAt != null) {
91 return;
92 }
93
94 resultSet.append(data);
95 this.recommendedDifficulties.set(filters.mode, data.recommended_difficulty);
96 }
97
98 @action
99 private clear() {
100 this.resultSets.clear();
101 this.recommendedDifficulties.clear();
102 }
103
104 private fetch(filters: BeatmapsetSearchFilters, from: number): PromiseLike<SearchResponse | null> {
105 this.cancel();
106
107 const params = filters.queryParams;
108 const key = filters.toKeyString();
109 const cursorString = this.getOrCreate(key).cursorString;
110
111 // undefined cursor should just do a cursorless query.
112 if (from > 0) {
113 if (cursorString != null) {
114 params.cursor_string = cursorString;
115 } else if (cursorString === null) {
116 return Promise.resolve(null);
117 }
118 }
119
120 const url = route('beatmapsets.search');
121 this.xhr = $.ajax(url, {
122 data: params,
123 dataType: 'json',
124 method: 'get',
125 });
126
127 return this.xhr;
128 }
129
130 private getOrCreate(key: string) {
131 let resultSet = this.resultSets.get(key);
132 if (resultSet == null) {
133 resultSet = new ResultSet();
134
135 this.resultSets.set(key, resultSet);
136 }
137
138 return resultSet;
139 }
140
141 private updateBeatmapsetStore(response: SearchResponse) {
142 for (const json of response.beatmapsets) {
143 this.beatmapsetStore.update(json);
144 }
145 }
146}