diff --git a/config/webpack.config.js b/config/webpack.config.js index 38018a9a40..fc40535aac 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -49,8 +49,6 @@ module.exports = [ }); } } - ], - - devtool: 'source-map' + ] } ]; diff --git a/src/core/Config.js b/src/core/Config.js index 9c3c731425..224a92dcdb 100644 --- a/src/core/Config.js +++ b/src/core/Config.js @@ -566,7 +566,7 @@ var Config = new Class({ /** * @const {number} Phaser.Core.Config#loaderMaxRetries - The number of times to retry a file load if it fails. */ - this.loaderMaxRetries = GetValue(config, 'loader.maxRetries', 2); + this.loaderMaxRetries = GetValue(config, 'loader.maxRetries', 5); /** * @const {boolean} Phaser.Core.Config#loaderWithCredentials - Optional XHR withCredentials value. diff --git a/src/core/typedefs/LoaderConfig.js b/src/core/typedefs/LoaderConfig.js index 22a87e548c..de1f8c297e 100644 --- a/src/core/typedefs/LoaderConfig.js +++ b/src/core/typedefs/LoaderConfig.js @@ -14,5 +14,5 @@ * @property {string[]} [localScheme] - An optional array of schemes that the Loader considers as being 'local' files. Defaults to: `[ 'file://', 'capacitor://' ]` if not specified. * @property {boolean} [withCredentials=false] - Optional XHR withCredentials value. * @property {string} [imageLoadType='XHR'] - Optional load type for image, `XHR` is default, or `HTMLImageElement` for a lightweight way. - * @property {number} [maxRetries=2] - The number of times to retry the file load if it fails. + * @property {number} [maxRetries=5] - The number of times to retry the file load if it fails. */ diff --git a/src/loader/File.js b/src/loader/File.js index a76c929369..35f04eaf21 100644 --- a/src/loader/File.js +++ b/src/loader/File.js @@ -247,23 +247,33 @@ var File = new Class({ this.base64 = (typeof url === 'string') && (url.indexOf('data:') === 0); /** - * The counter for the number of times to retry loading this file before it fails. - * + * The number of times to retry loading this file if it fails. + * * You can set this property value in the FileConfig object. If not present, * this property is read from the `LoaderPlugin.maxRetries` property when * this File instance is created. - * + * * You can set this value via the Game Config, or you can adjust the `LoaderPlugin` property * at any point after the Loader has started. However, it will not apply to files * that have already been added to the Loader, only those added after this value * is changed. * - * @name Phaser.Loader.File#retryAttempts + * @name Phaser.Loader.File#maxRetries + * @type {number} + * @default 5 + * @since 4.3.0 + */ + this.maxRetries = GetFastValue(this.xhrSettings, 'maxRetries', loader.maxRetries); + + /** + * The counter for the number of times loading has failed and was retried. + * + * @name Phaser.Loader.File#retries * @type {number} - * @default 2 - * @since 3.85.0 + * @default 0 + * @since 4.3.0 */ - this.retryAttempts = GetFastValue(fileConfig, 'maxRetries', loader.maxRetries); + this.retries = 0; }, /** @@ -363,7 +373,7 @@ var File = new Class({ this.resetXHR(); - this.loader.nextFile(this, success); + this.loader.nextFile(this, success, event); }, /** @@ -393,24 +403,29 @@ var File = new Class({ * if no retry attempts remain. * * @method Phaser.Loader.File#onError + * @fires Phaser.Loader.Events#FILE_LOAD_RETRY * @since 3.0.0 * * @param {XMLHttpRequest} xhr - The XMLHttpRequest that caused this onerror event. * @param {ProgressEvent} event - The DOM ProgressEvent that resulted from this error. */ - onError: function () + onError: function (xhr, event) { this.resetXHR(); - if (this.retryAttempts > 0) + if (this.retries < this.maxRetries) { - this.retryAttempts--; + var retryDelay = Math.min(Math.pow(2, this.retries) * 100, 5000); // ms + + this.retries++; + + this.loader.emit(Events.FILE_LOAD_RETRY, this, event, this.retries); - this.load(); + setTimeout(this.load.bind(this), retryDelay); } else { - this.loader.nextFile(this, false); + this.loader.nextFile(this, false, event); } }, diff --git a/src/loader/LoaderPlugin.js b/src/loader/LoaderPlugin.js index cdd1993b42..757e86614c 100644 --- a/src/loader/LoaderPlugin.js +++ b/src/loader/LoaderPlugin.js @@ -348,10 +348,10 @@ var LoaderPlugin = new Class({ /** * The number of times to retry loading a single file before it fails. - * + * * This property is read by the `File` object when it is created and set to * the internal property of the same name. It's not used by the Loader itself. - * + * * You can set this value via the Game Config, or you can adjust this property * at any point after the Loader has started. However, it will not apply to files * that have already been added to the Loader, only those added after this value @@ -359,7 +359,7 @@ var LoaderPlugin = new Class({ * * @name Phaser.Loader.LoaderPlugin#maxRetries * @type {number} - * @default 2 + * @default 5 * @since 3.85.0 */ this.maxRetries = GetFastValue(sceneConfig, 'maxRetries', gameConfig.loaderMaxRetries); @@ -1030,8 +1030,9 @@ var LoaderPlugin = new Class({ * * @param {Phaser.Loader.File} file - The File that just finished loading, or errored during load. * @param {boolean} success - `true` if the file loaded successfully, otherwise `false`. + * @param {Event | string} [event] - The Event that resulted from an error, if loading was not successful. */ - nextFile: function (file, success) + nextFile: function (file, success, event) { // Has the game been destroyed during load? If so, bail out now. if (!this.inflight) @@ -1059,7 +1060,7 @@ var LoaderPlugin = new Class({ this._deleteQueue.add(file); - this.emit(Events.FILE_LOAD_ERROR, file); + this.emit(Events.FILE_LOAD_ERROR, file, event); this.fileProcessComplete(file); } diff --git a/src/loader/events/FILE_LOAD_ERROR_EVENT.js b/src/loader/events/FILE_LOAD_ERROR_EVENT.js index b0599af5b5..7574fc0758 100644 --- a/src/loader/events/FILE_LOAD_ERROR_EVENT.js +++ b/src/loader/events/FILE_LOAD_ERROR_EVENT.js @@ -7,7 +7,7 @@ /** * The File Load Error Event. * - * This event is dispatched by the Loader Plugin when a file fails to load. + * This event is dispatched by the Loader Plugin when a file fails to load without retry. * * Listen to it from a Scene using: `this.load.on('loaderror', listener)`. * @@ -16,5 +16,6 @@ * @since 3.0.0 * * @param {Phaser.Loader.File} file - A reference to the File which errored during load. + * @param {Event | string} event - The Event that resulted from this error. */ module.exports = 'loaderror'; diff --git a/src/loader/events/FILE_LOAD_RETRY_EVENT.js b/src/loader/events/FILE_LOAD_RETRY_EVENT.js new file mode 100644 index 0000000000..b7d0bb4a39 --- /dev/null +++ b/src/loader/events/FILE_LOAD_RETRY_EVENT.js @@ -0,0 +1,23 @@ +/** + * @author Richard Davey + * @author Pavle Goloskokovic (http://prunegames.com) + * @copyright 2013-2026 Phaser Studio Inc. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +/** + * The File Load Retry Event. + * + * This event is dispatched by the Loader Plugin when a file fails to load but will retry loading. + * + * Listen to it from a Scene using: `this.load.on('loadretry', listener)`. + * + * @event Phaser.Loader.Events#FILE_LOAD_RETRY + * @type {string} + * @since 4.3.0 + * + * @param {Phaser.Loader.File} file - A reference to the File which errored during load, and is retrying. + * @param {ProgressEvent} event - The DOM ProgressEvent that resulted from this error. + * @param {number} retries - The number of times loading has failed and was retried. + */ +module.exports = 'loadretry'; diff --git a/src/loader/events/index.js b/src/loader/events/index.js index 6c3f129e8b..87e87fa436 100644 --- a/src/loader/events/index.js +++ b/src/loader/events/index.js @@ -14,6 +14,7 @@ module.exports = { COMPLETE: require('./COMPLETE_EVENT'), FILE_COMPLETE: require('./FILE_COMPLETE_EVENT'), FILE_KEY_COMPLETE: require('./FILE_KEY_COMPLETE_EVENT'), + FILE_LOAD_RETRY: require('./FILE_LOAD_RETRY_EVENT'), FILE_LOAD_ERROR: require('./FILE_LOAD_ERROR_EVENT'), FILE_LOAD: require('./FILE_LOAD_EVENT'), FILE_PROGRESS: require('./FILE_PROGRESS_EVENT'), diff --git a/src/loader/filetypes/HTML5AudioFile.js b/src/loader/filetypes/HTML5AudioFile.js index 7524515543..fe6a8feb3b 100644 --- a/src/loader/filetypes/HTML5AudioFile.js +++ b/src/loader/filetypes/HTML5AudioFile.js @@ -92,8 +92,10 @@ var HTML5AudioFile = new Class({ * * @method Phaser.Loader.FileTypes.HTML5AudioFile#onError * @since 3.0.0 + * + * @param {Event | string} event - The Event that resulted from this error. */ - onError: function () + onError: function (event) { for (var i = 0; i < this.data.length; i++) { @@ -103,7 +105,7 @@ var HTML5AudioFile = new Class({ audio.onerror = null; } - this.loader.nextFile(this, false); + this.loader.nextFile(this, false, event); }, /** diff --git a/src/loader/filetypes/ImageFile.js b/src/loader/filetypes/ImageFile.js index f5d807ace3..edbd3d8347 100644 --- a/src/loader/filetypes/ImageFile.js +++ b/src/loader/filetypes/ImageFile.js @@ -181,9 +181,9 @@ var ImageFile = new Class({ _this.loader.nextFile(_this, true); }; - this.data.onerror = function () + this.data.onerror = function (event) { - _this.loader.nextFile(_this, false); + _this.loader.nextFile(_this, false, event); }; this.data.src = this.src; diff --git a/src/loader/typedefs/FileConfig.js b/src/loader/typedefs/FileConfig.js index d6cc5b3231..fd4522bc1c 100644 --- a/src/loader/typedefs/FileConfig.js +++ b/src/loader/typedefs/FileConfig.js @@ -52,5 +52,4 @@ * @property {string} [systemKey] - If this plugin is to be added to Scene.Systems, this is the property key for it. * @property {string} [sceneKey] - If this plugin is to be added to the Scene, this is the property key for it. * @property {Phaser.Types.Loader.FileTypes.SVGSizeConfig} [svgConfig] - The svg size configuration object. - * @property {number} [maxRetries=2] - The number of times to retry the file load if it fails. */ diff --git a/src/loader/typedefs/XHRSettingsObject.js b/src/loader/typedefs/XHRSettingsObject.js index 01729d4e4e..c2ccef2856 100644 --- a/src/loader/typedefs/XHRSettingsObject.js +++ b/src/loader/typedefs/XHRSettingsObject.js @@ -12,5 +12,6 @@ * @property {(string|undefined)} [headerValue] - This value is used to populate the XHR `setRequestHeader` and is undefined by default. * @property {(string|undefined)} [requestedWith] - This value is used to populate the XHR `setRequestHeader` and is undefined by default. * @property {(string|undefined)} [overrideMimeType] - Provide a custom mime-type to use instead of the default. - * @property {boolean} [withCredentials=false] - The withCredentials property indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests. + * @property {boolean} [withCredentials=false] - The withCredentials property indicates whether cross-site Access-Control requests should be made using credentials such as cookies, authorization headers or TLS client certificates. Setting withCredentials has no effect on same-site requests. + * @property {number} [maxRetries=5] - The number of times to retry the file load if it fails. */ diff --git a/tests/loader/File.test.js b/tests/loader/File.test.js index a6c7e5efa8..6b6ef10f80 100644 --- a/tests/loader/File.test.js +++ b/tests/loader/File.test.js @@ -223,17 +223,23 @@ describe('File', function () expect(file.data).toBeUndefined(); }); - it('should use loader.maxRetries for retryAttempts when not in config', function () + it('should use loader.maxRetries when not in xhrSettings', function () { mockLoader.maxRetries = 5; var file = makeFile(); - expect(file.retryAttempts).toBe(5); + expect(file.maxRetries).toBe(5); }); - it('should use config maxRetries when provided', function () + it('should use xhrSettings maxRetries when provided', function () { - var file = makeFile({ maxRetries: 7 }); - expect(file.retryAttempts).toBe(7); + var file = makeFile({ xhrSettings: { maxRetries: 7 } }); + expect(file.maxRetries).toBe(7); + }); + + it('should initialise retries to 0', function () + { + var file = makeFile(); + expect(file.retries).toBe(0); }); it('should store cache reference from fileConfig', function () @@ -367,7 +373,7 @@ describe('File', function () file.onLoad(xhr, event); - expect(mockLoader.nextFile).toHaveBeenCalledWith(file, true); + expect(mockLoader.nextFile).toHaveBeenCalledWith(file, true, event); }); it('should call loader.nextFile with false on a 404 response', function () @@ -378,7 +384,7 @@ describe('File', function () file.onLoad(xhr, event); - expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false); + expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false, event); }); it('should call loader.nextFile with false on a 500 response', function () @@ -389,7 +395,7 @@ describe('File', function () file.onLoad(xhr, event); - expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false); + expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false, event); }); it('should call loader.nextFile with false on a 599 response', function () @@ -400,7 +406,7 @@ describe('File', function () file.onLoad(xhr, event); - expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false); + expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false, event); }); it('should treat a local file with status 0 as successful', function () @@ -412,7 +418,7 @@ describe('File', function () file.onLoad(xhr, event); - expect(loader.nextFile).toHaveBeenCalledWith(file, true); + expect(loader.nextFile).toHaveBeenCalledWith(file, true, event); }); it('should reset xhrLoader event handlers', function () @@ -484,32 +490,90 @@ describe('File', function () describe('onError', function () { - it('should decrement retryAttempts when attempts remain', function () + beforeEach(function () + { + vi.useFakeTimers(); + }); + + afterEach(function () + { + vi.useRealTimers(); + }); + + it('should increment retries when attempts remain', function () { var file = makeFile(); - file.retryAttempts = 3; + file.maxRetries = 3; file.load = vi.fn(); file.onError(); - expect(file.retryAttempts).toBe(2); + expect(file.retries).toBe(1); }); - it('should call load() when retry attempts remain', function () + it('should call load() after the retry delay when attempts remain', function () { var file = makeFile(); - file.retryAttempts = 1; + file.maxRetries = 1; file.load = vi.fn(); file.onError(); - expect(file.load).toHaveBeenCalled(); + expect(file.load).not.toHaveBeenCalled(); + + vi.runAllTimers(); + + expect(file.load).toHaveBeenCalledTimes(1); + }); + + it('should back off exponentially between retries', function () + { + var file = makeFile(); + file.maxRetries = 10; + file.load = vi.fn(); + + file.onError(); + vi.advanceTimersByTime(99); + expect(file.load).not.toHaveBeenCalled(); + vi.advanceTimersByTime(1); + expect(file.load).toHaveBeenCalledTimes(1); + + file.onError(); + vi.advanceTimersByTime(199); + expect(file.load).toHaveBeenCalledTimes(1); + vi.advanceTimersByTime(1); + expect(file.load).toHaveBeenCalledTimes(2); + }); + + it('should cap the retry delay at 5000ms', function () + { + var file = makeFile(); + file.maxRetries = 10; + file.retries = 9; + file.load = vi.fn(); + + file.onError(); + vi.advanceTimersByTime(5000); + + expect(file.load).toHaveBeenCalledTimes(1); + }); + + it('should emit FILE_LOAD_RETRY with the file, event and retry count', function () + { + var file = makeFile(); + file.maxRetries = 1; + file.load = vi.fn(); + var event = { type: 'error' }; + + file.onError({}, event); + + expect(mockLoader.emit).toHaveBeenCalledWith(Events.FILE_LOAD_RETRY, file, event, 1); }); it('should not call loader.nextFile when retrying', function () { var file = makeFile(); - file.retryAttempts = 1; + file.maxRetries = 1; file.load = vi.fn(); file.onError(); @@ -517,23 +581,35 @@ describe('File', function () expect(mockLoader.nextFile).not.toHaveBeenCalled(); }); - it('should call loader.nextFile with false when no retry attempts remain', function () + it('should call loader.nextFile with false and the event when no retry attempts remain', function () + { + var file = makeFile(); + file.maxRetries = 0; + var event = { type: 'error' }; + + file.onError({}, event); + + expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false, event); + }); + + it('should not emit FILE_LOAD_RETRY when no retry attempts remain', function () { var file = makeFile(); - file.retryAttempts = 0; + file.maxRetries = 0; file.onError(); - expect(mockLoader.nextFile).toHaveBeenCalledWith(file, false); + expect(mockLoader.emit).not.toHaveBeenCalledWith(Events.FILE_LOAD_RETRY, expect.anything(), expect.anything(), expect.anything()); }); it('should not call load() when no retry attempts remain', function () { var file = makeFile(); - file.retryAttempts = 0; + file.maxRetries = 0; file.load = vi.fn(); file.onError(); + vi.runAllTimers(); expect(file.load).not.toHaveBeenCalled(); }); @@ -541,7 +617,7 @@ describe('File', function () it('should reset xhrLoader event handlers before retrying', function () { var file = makeFile(); - file.retryAttempts = 1; + file.maxRetries = 1; file.load = vi.fn(); file.xhrLoader = { onload: function () {},