Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Fixed issue where `Resource.load()` would hang forever on network errors (DNS failure, CORS block, offline), deadlocking the loader and all scene navigation. The promise now rejects with a `ResourceLoadingError` containing the resource path and a descriptive message
- Fixed issue where `Gif.isLoaded()` always returned `true` because it checked `!!this.data` on an empty array, causing Gifs to be silently skipped by the loader and never parsed
- Fixed issue where `scaleTo({…})` and `scaleBy({…})` actions would cause entities to keep scaling indefinitely after the action completed, due to a copy-paste bug that zeroed `angularVelocity` instead of `scaleFactor`
- Fixed issue where `scaleTo({…})` and `scaleBy({…})` actions used a live reference to the entity's scale vector as the interpolation start point, causing the easing curve to be corrupted if the entity's scale changed during the action
- Fixed issue where the first action in a sequence would not execute after calling `clearActions()` mid-execution. All action types now properly reset their initialization state when stopped, resolving issue #3468
Expand Down
2 changes: 1 addition & 1 deletion src/engine/resources/gif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class Gif implements Loadable<ImageSource[]> {
}

public isLoaded() {
return !!this.data;
return this.data.length > 0;
}

/**
Expand Down
32 changes: 31 additions & 1 deletion src/engine/resources/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ export const ResourceEvents = {
Error: 'error'
};

/**
* Error thrown when a resource fails to load due to network issues,
* CORS problems, or the resource being unreachable.
*/
export class ResourceLoadingError extends Error {
constructor(
public path: string,
message: string
) {
super(message);
this.name = 'ResourceLoadingError';
}
}

/**
* The {@apilink Resource} type allows games built in Excalibur to load generic resources.
* For any type of remote resource it is recommended to use {@apilink Resource} for preloading.
Expand Down Expand Up @@ -73,7 +87,23 @@ export class Resource<T> implements Loadable<T> {
request.responseType = this.responseType;
request.addEventListener('loadstart', (e) => this.events.emit('loadstart', e as any));
request.addEventListener('progress', (e) => this.events.emit('progress', e as any));
request.addEventListener('error', (e) => this.events.emit('error', e as any));
request.addEventListener('error', (e) => {
this.events.emit('error', e as any);
reject(
new ResourceLoadingError(
this.path,
`Failed to load resource at ${this.path}. This may be a network error, CORS issue, or the resource is unreachable.`
)
);
});
request.addEventListener('abort', (e) => {
this.events.emit('error', e as any);
reject(new ResourceLoadingError(this.path, `Request to load resource at ${this.path} was aborted.`));
});
request.addEventListener('timeout', (e) => {
this.events.emit('error', e as any);
reject(new ResourceLoadingError(this.path, `Request to load resource at ${this.path} timed out.`));
});
request.addEventListener('load', (e) => this.events.emit('load', e as any));
request.addEventListener('load', () => {
// XHR on file:// success status is 0, such as with PhantomJS
Expand Down
10 changes: 10 additions & 0 deletions src/spec/vitest/gif-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ describe('A Gif', () => {
engine = null;
});

it('should not be loaded by default (C6 regression)', () => {
expect(gif.isLoaded()).toBe(false);
});

it('should be loaded after load() completes (C6 regression)', async () => {
expect(gif.isLoaded()).toBe(false);
await gif.load();
expect(gif.isLoaded()).toBe(true);
});

it('should parse gif files correctly', () =>
new Promise<void>((done) => {
gif.load().then(() => {
Expand Down
18 changes: 18 additions & 0 deletions src/spec/vitest/resource-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,22 @@ describe('A generic Resource', () => {
});
}));
});

describe('on network error', () => {
it('should reject with ResourceLoadingError on XHR error', () =>
new Promise<void>((done) => {
// Use a path that will trigger a network error (not a 404, but an actual XHR error)
// Using an invalid protocol triggers the XHR 'error' event
const badResource = new ex.Resource('file:///nonexistent/path/resource.png', 'blob');
badResource.load().then(
() => fail('Expected rejection'),
(err) => {
expect(err).toBeInstanceOf(ex.ResourceLoadingError);
expect(err.path).toBe('file:///nonexistent/path/resource.png');
expect(err.message).toContain('Failed to load resource');
done();
}
);
}));
});
});
Loading