Skip to content

Commit 1d5e06c

Browse files
authored
Fix loading of resources not properly retried when a resource can't be loaded (for example: network error) (#8060)
* Typically, when resources are downloaded from a CDN, then sometimes, for various reasons, a request for a resource may fail. In such cases, it’s very useful to have retry for avoiding intermittent issues.
1 parent e554ea5 commit 1d5e06c

File tree

8 files changed

+144
-114
lines changed

8 files changed

+144
-114
lines changed

Extensions/Spine/managers/pixi-spine-manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ namespace gdjs {
9090
logger.error(
9191
`Error while preloading spine resource ${resource.name}: ${error}`
9292
);
93+
PIXI.Assets.unload(resource.name);
94+
throw error;
9395
}
9496
}
9597

GDJS/Runtime/Model3DManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ namespace gdjs {
127127
logger.error(
128128
"Can't fetch the 3D model file " + resource.file + ', error: ' + error
129129
);
130+
throw error;
130131
}
131132
}
132133

GDJS/Runtime/ResourceLoader.ts

Lines changed: 85 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@ namespace gdjs {
2828
);
2929
};
3030

31-
const maxForegroundConcurrency = 20;
32-
const maxBackgroundConcurrency = 5;
33-
const maxAttempt = 3;
34-
3531
/**
3632
* A task of pre-loading resources used by a scene.
3733
*
@@ -104,11 +100,22 @@ namespace gdjs {
104100
}
105101
}
106102

103+
type PromiseError<T> = { item: T; error: Error };
104+
105+
type PromisePoolOutput<T, U> = {
106+
results: Array<U>;
107+
errors: Array<PromiseError<T>>;
108+
};
109+
107110
/**
108111
* Pre-load resources of any kind needed for a game or a scene.
109112
* @category Resources
110113
*/
111114
export class ResourceLoader {
115+
static maxForegroundConcurrency = 20;
116+
static maxBackgroundConcurrency = 5;
117+
static maxAttempt = 3;
118+
112119
_runtimeGame: RuntimeGame;
113120
/**
114121
* All the resource of a game by resource name.
@@ -291,10 +298,10 @@ namespace gdjs {
291298
onProgress: (loadingCount: integer, totalCount: integer) => void
292299
): Promise<void> {
293300
let loadedCount = 0;
294-
await processAndRetryIfNeededWithPromisePool(
301+
await ResourceLoader.processAndRetryIfNeededWithPromisePool(
295302
[...this._resources.values()],
296-
maxForegroundConcurrency,
297-
maxAttempt,
303+
ResourceLoader.maxForegroundConcurrency,
304+
ResourceLoader.maxAttempt,
298305
async (resource) => {
299306
await this._loadResource(resource);
300307
await this._processResource(resource);
@@ -313,10 +320,10 @@ namespace gdjs {
313320
onProgress: (loadingCount: integer, totalCount: integer) => void
314321
): Promise<void> {
315322
let loadedCount = 0;
316-
await processAndRetryIfNeededWithPromisePool(
323+
await ResourceLoader.processAndRetryIfNeededWithPromisePool(
317324
resourceNames,
318-
maxForegroundConcurrency,
319-
maxAttempt,
325+
ResourceLoader.maxForegroundConcurrency,
326+
ResourceLoader.maxAttempt,
320327
async (resourceName) => {
321328
const resource = this._resources.get(resourceName);
322329
if (resource) {
@@ -349,10 +356,10 @@ namespace gdjs {
349356
...this._globalResources,
350357
...firstSceneState.resourceNames,
351358
];
352-
await processAndRetryIfNeededWithPromisePool(
359+
await ResourceLoader.processAndRetryIfNeededWithPromisePool(
353360
resourceNames,
354-
maxForegroundConcurrency,
355-
maxAttempt,
361+
ResourceLoader.maxForegroundConcurrency,
362+
ResourceLoader.maxAttempt,
356363
async (resourceName) => {
357364
const resource = this._resources.get(resourceName);
358365
if (!resource) {
@@ -430,12 +437,12 @@ namespace gdjs {
430437
}
431438

432439
let loadedCount = 0;
433-
await processAndRetryIfNeededWithPromisePool(
440+
await ResourceLoader.processAndRetryIfNeededWithPromisePool(
434441
sceneState.resourceNames,
435442
this._isLoadingInForeground
436-
? maxForegroundConcurrency
437-
: maxBackgroundConcurrency,
438-
maxAttempt,
443+
? ResourceLoader.maxForegroundConcurrency
444+
: ResourceLoader.maxBackgroundConcurrency,
445+
ResourceLoader.maxAttempt,
439446
async (resourceName) => {
440447
const resource = this._resources.get(resourceName);
441448
if (!resource) {
@@ -882,80 +889,73 @@ namespace gdjs {
882889

883890
return result;
884891
}
885-
}
886-
887-
type PromiseError<T> = { item: T; error: Error };
888-
889-
type PromisePoolOutput<T, U> = {
890-
results: Array<U>;
891-
errors: Array<PromiseError<T>>;
892-
};
893892

894-
const processWithPromisePool = <T, U>(
895-
items: Array<T>,
896-
maxConcurrency: number,
897-
asyncFunction: (item: T) => Promise<U>
898-
): Promise<PromisePoolOutput<T, U>> => {
899-
const results: Array<U> = [];
900-
const errors: Array<PromiseError<T>> = [];
901-
let activePromises = 0;
902-
let index = 0;
903-
904-
return new Promise((resolve, reject) => {
905-
const executeNext = () => {
906-
if (items.length === 0) {
907-
resolve({ results, errors });
908-
return;
909-
}
910-
while (activePromises < maxConcurrency && index < items.length) {
911-
const item = items[index++];
912-
activePromises++;
913-
914-
asyncFunction(item)
915-
.then((result) => results.push(result))
916-
.catch((error) => errors.push({ item, error }))
917-
.finally(() => {
918-
activePromises--;
919-
if (index === items.length && activePromises === 0) {
920-
resolve({ results, errors });
921-
} else {
922-
executeNext();
923-
}
924-
});
925-
}
926-
};
927-
928-
executeNext();
929-
});
930-
};
893+
static processWithPromisePool<T, U>(
894+
items: Array<T>,
895+
maxConcurrency: number,
896+
asyncFunction: (item: T) => Promise<U>
897+
): Promise<PromisePoolOutput<T, U>> {
898+
const results: Array<U> = [];
899+
const errors: Array<PromiseError<T>> = [];
900+
let activePromises = 0;
901+
let index = 0;
902+
903+
return new Promise((resolve, reject) => {
904+
const executeNext = () => {
905+
if (items.length === 0) {
906+
resolve({ results, errors });
907+
return;
908+
}
909+
while (activePromises < maxConcurrency && index < items.length) {
910+
const item = items[index++];
911+
activePromises++;
912+
913+
asyncFunction(item)
914+
.then((result) => results.push(result))
915+
.catch((error) => errors.push({ item, error }))
916+
.finally(() => {
917+
activePromises--;
918+
if (index === items.length && activePromises === 0) {
919+
resolve({ results, errors });
920+
} else {
921+
executeNext();
922+
}
923+
});
924+
}
925+
};
931926

932-
const processAndRetryIfNeededWithPromisePool = async <T, U>(
933-
items: Array<T>,
934-
maxConcurrency: number,
935-
maxAttempt: number,
936-
asyncFunction: (item: T) => Promise<U>
937-
): Promise<PromisePoolOutput<T, U>> => {
938-
const output = await processWithPromisePool<T, U>(
939-
items,
940-
maxConcurrency,
941-
asyncFunction
942-
);
943-
if (output.errors.length !== 0) {
944-
logger.warn("Some assets couldn't be downloaded. Trying again now.");
927+
executeNext();
928+
});
945929
}
946-
for (
947-
let attempt = 1;
948-
attempt < maxAttempt && output.errors.length !== 0;
949-
attempt++
950-
) {
951-
const retryOutput = await processWithPromisePool<T, U>(
930+
931+
static async processAndRetryIfNeededWithPromisePool<T, U>(
932+
items: Array<T>,
933+
maxConcurrency: number,
934+
maxAttempt: number,
935+
asyncFunction: (item: T) => Promise<U>
936+
): Promise<PromisePoolOutput<T, U>> {
937+
const output = await ResourceLoader.processWithPromisePool<T, U>(
952938
items,
953939
maxConcurrency,
954940
asyncFunction
955941
);
956-
output.results.push.apply(output.results, retryOutput.results);
957-
output.errors = retryOutput.errors;
942+
if (output.errors.length !== 0) {
943+
logger.warn("Some assets couldn't be downloaded. Trying again now.");
944+
}
945+
for (
946+
let attempt = 1;
947+
attempt < maxAttempt && output.errors.length !== 0;
948+
attempt++
949+
) {
950+
const retryOutput = await ResourceLoader.processWithPromisePool<T, U>(
951+
items,
952+
maxConcurrency,
953+
asyncFunction
954+
);
955+
output.results.push.apply(output.results, retryOutput.results);
956+
output.errors = retryOutput.errors;
957+
}
958+
return output;
958959
}
959-
return output;
960-
};
960+
}
961961
}

GDJS/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ namespace gdjs {
195195
'): ' +
196196
(error.message || 'Unknown error')
197197
);
198+
throw error;
198199
}
199200
}
200201

GDJS/Runtime/howler-sound-manager/howler-sound-manager.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,13 @@ namespace gdjs {
292292
* @returns A float from 0 to 1.
293293
*/
294294
getVolume(): float {
295-
if (this._id === null) return this._initialVolume;
296-
return this._howl.volume(this._id);
295+
try {
296+
if (this._id === null) return this._initialVolume;
297+
return this._howl.volume(this._id);
298+
} catch (error) {
299+
handleHowlerSoundMethodError(error, 'getVolume');
300+
}
301+
return this._initialVolume;
297302
}
298303

299304
/**
@@ -1031,19 +1036,23 @@ namespace gdjs {
10311036
try {
10321037
await this._preloadAudioFile(resource, /* isMusic= */ true);
10331038
} catch (error) {
1039+
delete this._availableResources[resource.name];
10341040
logger.warn(
10351041
'There was an error while preloading an audio file: ' + error
10361042
);
1043+
throw error;
10371044
}
10381045
}
10391046

10401047
if (resource.preloadAsSound) {
10411048
try {
10421049
await this._preloadAudioFile(resource, /* isMusic= */ false);
10431050
} catch (error) {
1051+
delete this._availableResources[resource.name];
10441052
logger.warn(
10451053
'There was an error while preloading an audio file: ' + error
10461054
);
1055+
throw error;
10471056
}
10481057
} else if (
10491058
resource.preloadInCache ||
@@ -1061,7 +1070,15 @@ namespace gdjs {
10611070
const sound = new XMLHttpRequest();
10621071
sound.withCredentials =
10631072
this._resourceLoader.checkIfCredentialsRequired(file);
1064-
sound.addEventListener('load', resolve);
1073+
sound.addEventListener('load', () => {
1074+
if (sound.status >= 200 && sound.status < 300) {
1075+
resolve(undefined);
1076+
} else {
1077+
reject(
1078+
`HTTP error while preloading audio file in cache. Status is ${sound.status}.`
1079+
);
1080+
}
1081+
});
10651082
sound.addEventListener('error', (_) =>
10661083
reject('XHR error: ' + file)
10671084
);
@@ -1072,9 +1089,11 @@ namespace gdjs {
10721089
sound.send();
10731090
});
10741091
} catch (error) {
1092+
delete this._availableResources[resource.name];
10751093
logger.warn(
10761094
'There was an error while preloading an audio file: ' + error
10771095
);
1096+
throw error;
10781097
}
10791098
}
10801099
}

GDJS/Runtime/jsonmanager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ namespace gdjs {
6565
`Error while preloading json resource ${resource.name}:`,
6666
error
6767
);
68+
throw error;
6869
}
6970
}
7071

GDJS/Runtime/pixi-renderers/pixi-bitmapfont-manager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,12 @@ namespace gdjs {
279279
'same-origin',
280280
}
281281
);
282+
if (!response.ok) {
283+
throw new Error(
284+
`HTTP error while loading bitmap font. Status is ${response.status}.`
285+
);
286+
}
287+
282288
const fontDataRaw = await response.text();
283289

284290
// Sanitize: remove lines starting with # (acting as comments)
@@ -295,6 +301,8 @@ namespace gdjs {
295301
', error: ' +
296302
error
297303
);
304+
this._loadedFontsData.delete(resource);
305+
throw error;
298306
}
299307
}
300308

0 commit comments

Comments
 (0)