Skip to content

Commit 89056a0

Browse files
arturovtAndrewKushnir
authored andcommitted
fix(common): cleanup updateLatestValue if view is destroyed before promise resolves (#61064)
Patch version of #58041. PR Close #61064
1 parent e11ded1 commit 89056a0

1 file changed

Lines changed: 42 additions & 6 deletions

File tree

packages/common/src/pipes/async_pipe.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
ɵisPromise,
1717
ɵisSubscribable,
1818
} from '@angular/core';
19-
import {Observable, Subscribable, Unsubscribable} from 'rxjs';
19+
import type {Observable, Subscribable, Unsubscribable} from 'rxjs';
2020

2121
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';
2222

@@ -54,13 +54,49 @@ class SubscribableStrategy implements SubscriptionStrategy {
5454
}
5555

5656
class PromiseStrategy implements SubscriptionStrategy {
57-
createSubscription(async: Promise<any>, updateLatestValue: (v: any) => any): Promise<any> {
58-
return async.then(updateLatestValue, (e) => {
59-
throw e;
60-
});
57+
createSubscription(
58+
async: Promise<any>,
59+
updateLatestValue: ((v: any) => any) | null,
60+
): Unsubscribable {
61+
// According to the promise specification, promises are not cancellable by default.
62+
// Once a promise is created, it will either resolve or reject, and it doesn't
63+
// provide a built-in mechanism to cancel it.
64+
// There may be situations where a promise is provided, and it either resolves after
65+
// the pipe has been destroyed or never resolves at all. If the promise never
66+
// resolves — potentially due to factors beyond our control, such as third-party
67+
// libraries — this can lead to a memory leak.
68+
// When we use `async.then(updateLatestValue)`, the engine captures a reference to the
69+
// `updateLatestValue` function. This allows the promise to invoke that function when it
70+
// resolves. In this case, the promise directly captures a reference to the
71+
// `updateLatestValue` function. If the promise resolves later, it retains a reference
72+
// to the original `updateLatestValue`, meaning that even if the context where
73+
// `updateLatestValue` was defined has been destroyed, the function reference remains in memory.
74+
// This can lead to memory leaks if `updateLatestValue` is no longer needed or if it holds
75+
// onto resources that should be released.
76+
// When we do `async.then(v => ...)` the promise captures a reference to the lambda
77+
// function (the arrow function).
78+
// When we assign `updateLatestValue = null` within the context of an `unsubscribe` function,
79+
// we're changing the reference of `updateLatestValue` in the current scope to `null`.
80+
// The lambda will no longer have access to it after the assignment, effectively
81+
// preventing any further calls to the original function and allowing it to be garbage collected.
82+
async.then(
83+
// Using optional chaining because we may have set it to `null`; since the promise
84+
// is async, the view might be destroyed by the time the promise resolves.
85+
(v) => updateLatestValue?.(v),
86+
(e) => {
87+
throw e;
88+
},
89+
);
90+
return {
91+
unsubscribe: () => {
92+
updateLatestValue = null;
93+
},
94+
};
6195
}
6296

63-
dispose(subscription: Promise<any>): void {}
97+
dispose(subscription: Unsubscribable): void {
98+
subscription.unsubscribe();
99+
}
64100
}
65101

66102
const _promiseStrategy = new PromiseStrategy();

0 commit comments

Comments
 (0)