Skip to content

Commit fb41f15

Browse files
committed
Skip unnecessary steps when downloading an unchanged mod list
In SplashMixin, skip downloading the chunks and updating them into the IndexedDB if the index file indicates the packages haven't changed. If downloading the index file itself fails, try to load earlier mod list from the IndexedDB like it did earlier. In UtilityMixin, skip the whole process if the index file indicates the packages haven't changed, as at this point we already have the mod list loaded in to Vuex. If loading the index file itself fails, the error is caught by the caller, which handles retries as it did earlier.
1 parent 9d9d3fb commit fb41f15

File tree

3 files changed

+73
-28
lines changed

3 files changed

+73
-28
lines changed

src/components/mixins/SplashMixin.vue

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Component from 'vue-class-component';
44
55
import R2Error from '../../model/errors/R2Error';
66
import RequestItem from '../../model/requests/RequestItem';
7-
import type { PackageListChunks } from '../../store/modules/TsModsModule';
7+
import type { PackageListChunks, PackageListIndex } from '../../store/modules/TsModsModule';
88
99
1010
@Component
@@ -51,16 +51,19 @@ export default class SplashMixin extends Vue {
5151
5252
// Get the list of Thunderstore mods from API or local cache.
5353
async getThunderstoreMods() {
54-
const packageListChunks = await this.fetchPackageListChunksIfUpdated();
54+
const packageListIndex = await this.fetchPackageListIndex();
55+
const packageListChunks = await this.fetchPackageListChunksIfUpdated(packageListIndex);
5556
this.getRequestItem('ThunderstoreDownload').setProgress(100);
56-
await this.writeModsToPersistentCacheIfUpdated(packageListChunks);
57+
await this.writeModsToPersistentCacheIfUpdated(packageListIndex, packageListChunks);
5758
const isModListLoaded = await this.readModsToVuex();
5859
5960
// To proceed, the loading of the mod list should result in a non-empty list.
6061
// Empty list is allowed if that's actually what the API returned.
62+
// API wasn't queried at all if we already had the latest index chunk.
6163
const modListHasMods = this.$store.state.tsMods.mods.length;
6264
const apiReturnedEmptyList = packageListChunks && packageListChunks[0].length === 0;
63-
if (isModListLoaded && (modListHasMods || apiReturnedEmptyList)) {
65+
const apiWasNotQueried = packageListIndex && packageListIndex.isLatest;
66+
if (isModListLoaded && (modListHasMods || apiReturnedEmptyList || apiWasNotQueried)) {
6467
await this.moveToNextScreen();
6568
} else {
6669
this.heroTitle = 'Failed to get the list of online mods';
@@ -70,18 +73,41 @@ export default class SplashMixin extends Vue {
7073
}
7174
7275
/***
73-
* Load the package list in chunks.
76+
* Query Thunderstore API for the URLs pointing to parts of the package list.
7477
* Fails silently to fallback reading the old values from the IndexedDB cache.
7578
*/
76-
async fetchPackageListChunksIfUpdated(): Promise<PackageListChunks|undefined> {
79+
async fetchPackageListIndex(): Promise<PackageListIndex|undefined> {
80+
this.loadingText = 'Checking for mod list updates from Thunderstore';
7781
78-
const showProgress = (progress: number) => {
82+
try {
83+
return await this.$store.dispatch('tsMods/fetchPackageListIndex');
84+
} catch (e) {
85+
console.error('SplashMixin failed to fetch mod list index from API.', e);
86+
return undefined;
87+
}
88+
}
89+
90+
/***
91+
* Load the package list in chunks pointed out by the packageListIndex.
92+
* This step is skipped if we can't or don't need to load the chunks.
93+
* Fails silently to fallback reading the old values from the IndexedDB cache.
94+
*/
95+
async fetchPackageListChunksIfUpdated(packageListIndex?: PackageListIndex): Promise<PackageListChunks|undefined> {
96+
// Skip loading chunks if loading index failed, or if we already have the latest data.
97+
if (!packageListIndex || packageListIndex.isLatest) {
98+
return undefined;
99+
}
100+
101+
const progressCallback = (progress: number) => {
79102
this.loadingText = 'Loading latest mod list from Thunderstore';
80103
this.getRequestItem('ThunderstoreDownload').setProgress(progress);
81104
};
82105
83106
try {
84-
return await this.$store.dispatch('tsMods/fetchPackageListChunks', showProgress);
107+
return await this.$store.dispatch(
108+
'tsMods/fetchPackageListChunks',
109+
{chunkUrls: packageListIndex.content, progressCallback}
110+
);
85111
} catch (e) {
86112
console.error('SplashMixin failed to fetch mod list from API.', e);
87113
return undefined;
@@ -90,15 +116,18 @@ export default class SplashMixin extends Vue {
90116
91117
/***
92118
* Update a fresh package list to the IndexedDB cache.
93-
* Done only if the package list was loaded successfully.
119+
* Done only if there was a fresh list to load and it was loaded successfully.
94120
* Fails silently to fallback reading the old values from the IndexedDB cache.
95121
*/
96-
async writeModsToPersistentCacheIfUpdated(packageListChunks?: PackageListChunks) {
97-
if (packageListChunks) {
122+
async writeModsToPersistentCacheIfUpdated(packageListIndex?: PackageListIndex, packageListChunks?: PackageListChunks) {
123+
if (packageListIndex && packageListChunks) {
98124
this.loadingText = 'Storing the mod list into local cache';
99125
100126
try {
101-
await this.$store.dispatch('tsMods/updatePersistentCache', packageListChunks)
127+
await this.$store.dispatch(
128+
'tsMods/updatePersistentCache',
129+
{indexHash: packageListIndex.hash, chunks: packageListChunks}
130+
);
102131
} catch (e) {
103132
console.error('SplashMixin failed to cache mod list locally.', e);
104133
}
@@ -117,12 +146,12 @@ export default class SplashMixin extends Vue {
117146
* queried from the API successfully or not. This also handles the type
118147
* casting, since mod manager expects the data to be formatted into objects.
119148
*
149+
* Failure at this point is no longer silently ignored, instead an error
150+
* modal is shown.
151+
*
120152
* Return value is used to tell whether Vuex might contain an empty list
121153
* after calling this because there was an error, or because the package
122154
* list is actually empty.
123-
*
124-
* Failure at this point is no longer silently ignored, instead an error
125-
* modal is shown.
126155
*/
127156
async readModsToVuex(): Promise<boolean> {
128157
let isModListLoaded = false;

src/components/mixins/UtilityMixin.vue

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Component from 'vue-class-component';
44
55
import R2Error from '../../model/errors/R2Error';
66
import CdnProvider from '../../providers/generic/connection/CdnProvider';
7+
import { PackageListIndex } from '../../store/modules/TsModsModule';
78
89
@Component
910
export default class UtilityMixin extends Vue {
@@ -15,8 +16,20 @@ export default class UtilityMixin extends Vue {
1516
}
1617
1718
async refreshThunderstoreModList() {
18-
const packageListChunks = await this.$store.dispatch('tsMods/fetchPackageListChunks');
19-
await this.$store.dispatch("tsMods/updatePersistentCache", packageListChunks);
19+
const packageListIndex: PackageListIndex = await this.$store.dispatch("tsMods/fetchPackageListIndex");
20+
21+
if (packageListIndex.isLatest) {
22+
return;
23+
}
24+
25+
const packageListChunks = await this.$store.dispatch(
26+
"tsMods/fetchPackageListChunks",
27+
{chunkUrls: packageListIndex.content},
28+
);
29+
await this.$store.dispatch(
30+
"tsMods/updatePersistentCache",
31+
{chunks: packageListChunks, indexHash: packageListIndex.hash},
32+
);
2033
await this.$store.dispatch("tsMods/updateMods");
2134
await this.$store.dispatch("profile/tryLoadModListFromDisk");
2235
await this.$store.dispatch("tsMods/prewarmCache");

src/store/modules/TsModsModule.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface State {
2929
type ProgressCallback = (progress: number) => void;
3030
type PackageListChunk = {full_name: string}[];
3131
export type PackageListChunks = PackageListChunk[];
32+
export type PackageListIndex = {content: string[], hash: string, isLatest: boolean};
3233

3334
function isPackageListChunk(value: unknown): value is PackageListChunk {
3435
return Array.isArray(value) && (
@@ -161,7 +162,7 @@ export const TsModsModule = {
161162
},
162163

163164
actions: <ActionTree<State, RootState>>{
164-
async _fetchPackageListIndex({rootState}): Promise<string[]> {
165+
async fetchPackageListIndex({rootState}): Promise<PackageListIndex> {
165166
const indexUrl = rootState.activeGame.thunderstoreUrl;
166167
const index = await retry(() => fetchAndProcessBlobFile(indexUrl));
167168

@@ -172,28 +173,29 @@ export const TsModsModule = {
172173
throw new Error('Received empty chunk index from API');
173174
}
174175

175-
return index.content;
176+
const community = rootState.activeGame.internalFolderName;
177+
const isLatest = await PackageDb.isLatestPackageListIndex(community, index.hash);
178+
179+
return {...index, isLatest};
176180
},
177181

178182
async fetchPackageListChunks(
179-
{dispatch},
180-
progressCallback?: ProgressCallback
183+
{},
184+
{chunkUrls, progressCallback}: {chunkUrls: string[], progressCallback?: ProgressCallback},
181185
): Promise<PackageListChunks> {
182-
const chunkIndex: string[] = await dispatch('_fetchPackageListIndex');
183-
184186
// Count index as a chunk for progress bar purposes.
185-
const chunkCount = chunkIndex.length + 1;
187+
const chunkCount = chunkUrls.length + 1;
186188
let completed = 1;
187189
const updateProgress = () => progressCallback && progressCallback((completed / chunkCount) * 100);
188190
updateProgress();
189191

190192
// Download chunks serially to avoid slow connections timing
191193
// out due to concurrent requests competing for the bandwidth.
192194
const chunks = [];
193-
for (const [i, chunkUrl] of chunkIndex.entries()) {
195+
for (const [i, chunkUrl] of chunkUrls.entries()) {
194196
const {content: chunk} = await retry(() => fetchAndProcessBlobFile(chunkUrl))
195197

196-
if (chunkIndex.length > 1 && isEmptyArray(chunkIndex)) {
198+
if (chunkUrls.length > 1 && isEmptyArray(chunk)) {
197199
throw new Error(`Chunk #${i} in multichunk response was empty`);
198200
} else if (!isPackageListChunk(chunk)) {
199201
throw new Error(`Chunk #${i} was invalid format`);
@@ -232,17 +234,18 @@ export const TsModsModule = {
232234
/*** Save a mod list received from the Thunderstore API to IndexedDB */
233235
async updatePersistentCache(
234236
{dispatch, rootState, state},
235-
packages: PackageListChunks
237+
{chunks, indexHash}: {chunks: PackageListChunks, indexHash: string}
236238
) {
237239
if (state.exclusions === undefined) {
238240
await dispatch('updateExclusions');
239241
}
240242

241-
const filtered = packages.map((chunk) => chunk.filter(
243+
const filtered = chunks.map((chunk) => chunk.filter(
242244
(pkg) => !state.exclusions!.includes(pkg.full_name)
243245
));
244246
const community = rootState.activeGame.internalFolderName;
245247
await PackageDb.updateFromApiResponse(community, filtered);
248+
await PackageDb.setLatestPackageListIndex(community, indexHash);
246249
}
247250
}
248251
}

0 commit comments

Comments
 (0)