Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
Not established. A related streaming-resource cancellation report (#64625) was closed by its author without an associated fix.
Description
When a resource has an outstanding loader and its params become unavailable, the resource immediately changes status but does not abort the obsolete loader or release that loader's pending task. If the obsolete promise never settles, ApplicationRef.whenStable() remains pending indefinitely even though the resource is now idle or in a params-error state.
This affects params returning undefined, throwing ResourceParamsStatus.IDLE or ResourceParamsStatus.LOADING, and throwing an error. In the LOADING case, the resource is waiting on upstream params; its obsolete downstream request should no longer own a pending task.
The resource guide says outstanding loads are aborted when params change. This differs from the expected behavior of blocking stability while a valid current request is loading (as discussed in #60742).
Minimal reproduction
Executable regression tests: resource_spec.ts, with unavailable/error status cases in params_status_spec.ts. Run pnpm bazel test //packages/core/test/resource:resource; keeping the tests and restoring the pre-fix resource.ts reproduces the blocked stability calls.
Use the following as src/main.ts in an Angular application, wait for loader started in the console, then click Stop:
import {ApplicationRef, Component, inject, resource, signal} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
@Component({
selector: 'app-root',
template: `
<button (click)="stop()">Stop</button>
<p>Status: {{data.status()}}; aborted: {{aborted()}}</p>
`,
})
class App {
readonly params = signal<number | undefined>(1);
readonly aborted = signal(false);
private readonly app = inject(ApplicationRef);
readonly data = resource({
params: this.params,
loader: ({abortSignal}) => {
console.log('loader started');
abortSignal.addEventListener('abort', () => this.aborted.set(true));
// Cancellation must release stability even if a loader never settles.
return new Promise<string>(() => {});
},
});
stop() {
this.params.set(undefined);
void this.app.whenStable().then(() => console.log('stable after stop'));
}
}
void bootstrapApplication(App);Expected: Status: idle; aborted: true, and stable after stop is logged.
Actual: Status: idle; aborted: false, and the stability promise never resolves.
Environment
Angular main source commit fdb4517612cf191da3c3e2df251f9f79f026448b (22.3.0-next.3), macOS arm64. The repository's Node/Bazel resource suite is used to verify the behavior.
Anything else?
ResourceImpl.loadEffect() checks unavailable/stale params before calling abortInProgressLoad(). Cancelling before those early returns fixes the omitted cleanup while preserving the existing request identity checks. Regression tests also cover an obsolete promise resolving/rejecting after a new load starts, ensuring the new load retains its pending task and value.
Related: #64625. PR #71557 changes abort reasons/controller retention, but does not fix this early-return path.
Activity