diff --git a/packages/common/src/pipes/async_pipe.ts b/packages/common/src/pipes/async_pipe.ts index 80f109923dc..6571e2d2c41 100644 --- a/packages/common/src/pipes/async_pipe.ts +++ b/packages/common/src/pipes/async_pipe.ts @@ -18,7 +18,7 @@ import { ɵINTERNAL_APPLICATION_ERROR_HANDLER as INTERNAL_APPLICATION_ERROR_HANDLER, inject, } from '@angular/core'; -import {Observable, Subscribable, Unsubscribable} from 'rxjs'; +import type {Observable, Subscribable, Unsubscribable} from 'rxjs'; import {invalidPipeArgumentError} from './invalid_pipe_argument_error'; @@ -61,13 +61,47 @@ class SubscribableStrategy implements SubscriptionStrategy { class PromiseStrategy implements SubscriptionStrategy { createSubscription( async: PromiseLike, - updateLatestValue: (v: any) => any, - onError: (e: unknown) => void, - ): PromiseLike { - return async.then(updateLatestValue, onError); + updateLatestValue: ((v: any) => any) | null, + onError: ((e: unknown) => void) | null, + ): Unsubscribable { + // According to the promise specification, promises are not cancellable by default. + // Once a promise is created, it will either resolve or reject, and it doesn't + // provide a built-in mechanism to cancel it. + // There may be situations where a promise is provided, and it either resolves after + // the pipe has been destroyed or never resolves at all. If the promise never + // resolves — potentially due to factors beyond our control, such as third-party + // libraries — this can lead to a memory leak. + // When we use `async.then(updateLatestValue)`, the engine captures a reference to the + // `updateLatestValue` function. This allows the promise to invoke that function when it + // resolves. In this case, the promise directly captures a reference to the + // `updateLatestValue` function. If the promise resolves later, it retains a reference + // to the original `updateLatestValue`, meaning that even if the context where + // `updateLatestValue` was defined has been destroyed, the function reference remains in memory. + // This can lead to memory leaks if `updateLatestValue` is no longer needed or if it holds + // onto resources that should be released. + // When we do `async.then(v => ...)` the promise captures a reference to the lambda + // function (the arrow function). + // When we assign `updateLatestValue = null` within the context of an `unsubscribe` function, + // we're changing the reference of `updateLatestValue` in the current scope to `null`. + // The lambda will no longer have access to it after the assignment, effectively + // preventing any further calls to the original function and allowing it to be garbage collected. + async.then( + // Using optional chaining because we may have set it to `null`; since the promise + // is async, the view might be destroyed by the time the promise resolves. + (v) => updateLatestValue?.(v), + (e) => onError?.(e), + ); + return { + unsubscribe: () => { + updateLatestValue = null; + onError = null; + }, + }; } - dispose(subscription: PromiseLike): void {} + dispose(subscription: Unsubscribable): void { + subscription.unsubscribe(); + } } const _promiseStrategy = new PromiseStrategy();