Skip to content

Commit

Permalink
fix: attach polymer binding callback to a promise that always resolves (
Browse files Browse the repository at this point in the history
#14729) (#14755)

Polymer binding callback was attached to the promise returned by
customElements.whenDefined, but this promise may never complete
if the input element is not a custom element, causing memory leaks
on browser because of element capture.
This change introduces a new promise that completes either when
whenDefined is fulfilled or after a fixed timeout, allowing
the garbage collector to clean resources.

Fixes #14422

Co-authored-by: Marco Collovati <marco@vaadin.com>
  • Loading branch information
vaadin-bot and mcollovati committed Oct 7, 2022
1 parent 8660bad commit 104a06f
Show file tree
Hide file tree
Showing 5 changed files with 30 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,15 @@ private native void bindPolymerModelProperties(StateNode node,
} else if ( @com.vaadin.client.PolymerUtils::mayBePolymerElement(*)(element) ) {
var self = this;
try {
$wnd.customElements.whenDefined(element.localName).then( function () {
var whenDefinedPromise = $wnd.customElements.whenDefined(element.localName);
var promiseTimeout = new Promise(function(r) { setTimeout(r, 1000); });
// if element is not a web component, the promise returned by
// whenDefined may never complete, causing memory leaks because of
// closures in chained function.
// Using `Promise.race` with a secondary promise that resolves after
// a defined interval and chaining on this one, will always resolve,
// execute the function and allow the garbage collector to free resources
Promise.race([whenDefinedPromise, promiseTimeout]).then( function () {
if ( @com.vaadin.client.PolymerUtils::isPolymerElement(*)(element) ) {
self.@SimpleElementBindingStrategy::hookUpPolymerElement(*)(node, element);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ private static native void installPolyfills()
// promise-polyfill 8.1.3
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n():"function"==typeof define&&define.amd?define(n):n()}(0,function(){"use strict";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(e){return!(!e||"undefined"==typeof e.length)}function t(){}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],c(e,this)}function r(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(r){return void f(n.promise,r)}i(n.promise,o)}else(1===e._state?i:f)(n.promise,e._value)})):e._deferreds.push(n)}function i(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if(n instanceof o)return e._state=3,e._value=n,void u(e);if("function"==typeof t)return void c(function(e,n){return function(){e.apply(n,arguments)}}(t,n),e)}e._state=1,e._value=n,u(e)}catch(r){f(e,r)}}function f(e,n){e._state=2,e._value=n,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;t>n;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype["finally"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=o});
// Run promise callbacks immediately in tests
// Keep also original _immediateFn so it can be restored if needed
window.Promise._originalImmediateFn = window.Promise._immediateFn;
window.Promise._immediateFn = function(callback) { callback(); };
}-*/;

}
Original file line number Diff line number Diff line change
Expand Up @@ -1955,19 +1955,17 @@ private native void mockWhenDefined(Element element)
$wnd.Polymer = null;
$wnd.customElements = {
whenDefined: function() {
return {
then: function (callback) {
element.callback = callback;
}
}
return new Promise(function(resolve) {
element.completeWhenDefinedPromise = resolve;
});
}
};
}-*/;

private native void runWhenDefined(Element element)
/*-{
$wnd.Polymer = $wnd.OldPolymer;
element.callback();
element.completeWhenDefinedPromise();
}-*/;

private native void initPolymer(Element element)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class GwtEventHandlerTest extends ClientEngineTestBase {
@Override
protected void gwtSetUp() throws Exception {
super.gwtSetUp();
restorePromiseImmediateFn();
Reactive.reset();

registry = new Registry() {
Expand Down Expand Up @@ -141,6 +142,11 @@ public void testClientCallablePromises() {
delayTestFinish(100);
}

private static native void restorePromiseImmediateFn()
/*-{
window.Promise._immediateFn = window.Promise._originalImmediateFn;
}-*/;

private static native void addThen(Object promise,
Consumer<String> callback)
/*-{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import com.google.gwt.core.client.impl.SchedulerImpl;

import com.vaadin.client.ClientEngineTestBase;
import com.vaadin.client.CustomScheduler;
import com.vaadin.client.PolymerUtils;
import com.vaadin.client.WidgetUtil;
Expand Down Expand Up @@ -407,17 +408,14 @@ private native void addMockMethods(Element element)
element._propertiesChanged = function() {
element.propertiesChangedCallCount += 1;
};
element.callbackCallCount = 0;
$wnd.customElements = {
whenDefined: function() {
return {
then: function (callback) {
$wnd.Polymer = $wnd.OldPolymer;
element.callbackCallCount += 1;
callback();
}
}
return new Promise(function(resolve) {
$wnd.Polymer = $wnd.OldPolymer;
element.callbackCallCount += 1;
resolve();
});
}
};
if( !element.removeAttribute ) {
Expand Down

0 comments on commit 104a06f

Please sign in to comment.