Replace Polymer Legacy this.debounce with simple debounce utility

Change-Id: I23e4388d047062cb093144978d0c3a646bf8517f
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index a1ab580..883e518 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -174,6 +174,7 @@
 import {aPluginHasRegistered$} from '../../../services/checks/checks-model';
 import {Subject} from 'rxjs';
 import {GrRelatedChangesListExperimental} from '../gr-related-changes-list-experimental/gr-related-changes-list-experimental';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -244,10 +245,6 @@
 
 export type ChangeViewPatchRange = Partial<PatchRange>;
 
-const DEBOUNCER_REPLY_OVERLAY_REFIT = 'reply-overlay-refit';
-
-const DEBOUNCER_SCROLL = 'scroll';
-
 @customElement('gr-change-view')
 export class GrChangeView extends KeyboardShortcutMixin(
   LegacyElementMixin(PolymerElement)
@@ -593,6 +590,10 @@
 
   disconnected$ = new Subject();
 
+  private replyRefitTask?: DelayedTask;
+
+  private scrollTask?: DelayedTask;
+
   /** @override */
   ready() {
     super.ready();
@@ -715,8 +716,8 @@
       'visibilitychange',
       this.handleVisibilityChange
     );
-    this.cancelDebouncer(DEBOUNCER_REPLY_OVERLAY_REFIT);
-    this.cancelDebouncer(DEBOUNCER_SCROLL);
+    this.replyRefitTask?.cancel();
+    this.scrollTask?.cancel();
 
     if (this._updateCheckTimerHandle) {
       this._cancelUpdateCheckTimer();
@@ -1241,11 +1242,9 @@
 
   _handleReplyAutogrow() {
     // If the textarea resizes, we need to re-fit the overlay.
-    this.debounce(
-      DEBOUNCER_REPLY_OVERLAY_REFIT,
-      () => {
-        this.$.replyOverlay.refit();
-      },
+    this.replyRefitTask = debounce(
+      this.replyRefitTask,
+      () => this.$.replyOverlay.refit(),
       REPLY_REFIT_DEBOUNCE_INTERVAL_MS
     );
   }
@@ -1259,11 +1258,9 @@
   }
 
   readonly handleScroll = () => {
-    this.debounce(
-      DEBOUNCER_SCROLL,
-      () => {
-        this.viewState.scrollTop = document.body.scrollTop;
-      },
+    this.scrollTask = debounce(
+      this.scrollTask,
+      () => (this.viewState.scrollTop = document.body.scrollTop),
       150
     );
   };
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index defaf0fd..7c5d30f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -31,7 +31,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
 import {htmlTemplate} from './gr-file-list_html';
-import {asyncForeach} from '../../../utils/async-util';
+import {asyncForeach, debounce, DelayedTask} from '../../../utils/async-util';
 import {
   KeyboardShortcutMixin,
   Modifier,
@@ -154,8 +154,6 @@
 
 export type FileNameToReviewedFileInfoMap = {[name: string]: ReviewedFileInfo};
 
-const DEBOUNCER_LOADING_CHANGE = 'loading-change';
-
 /**
  * Type for FileInfo
  *
@@ -283,6 +281,8 @@
 
   private _cancelForEachDiff?: () => void;
 
+  loadingTask?: DelayedTask;
+
   @property({
     type: Boolean,
     computed:
@@ -418,7 +418,7 @@
   disconnectedCallback() {
     this.fileCursor.unsetCursor();
     this._cancelDiffs();
-    this.cancelDebouncer(DEBOUNCER_LOADING_CHANGE);
+    this.loadingTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -1606,8 +1606,8 @@
    * are reasonably fast.
    */
   _loadingChanged(loading?: boolean) {
-    this.debounce(
-      DEBOUNCER_LOADING_CHANGE,
+    this.loadingTask = debounce(
+      this.loadingTask,
       () => {
         // Only show set the loading if there have been files loaded to show. In
         // this way, the gray loading style is not shown on initial loads.
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index ce20435..a1e227b 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -1103,13 +1103,13 @@
 
       element.reload().then(() => {
         assert.isFalse(element._loading);
-        element.flushDebouncer('loading-change');
+        element.loadingTask.flush();
         assert.isFalse(element.classList.contains('loading'));
         done();
       });
       assert.isTrue(element._loading);
       assert.isFalse(element.classList.contains('loading'));
-      element.flushDebouncer('loading-change');
+      element.loadingTask.flush();
       assert.isTrue(element.classList.contains('loading'));
     });
 
@@ -1119,7 +1119,7 @@
       element.patchRange = {patchNum: 12};
       element.reload();
       assert.isTrue(element._loading);
-      element.flushDebouncer('loading-change');
+      element.loadingTask.flush();
       assert.isFalse(element.classList.contains('loading'));
     });
   });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index a735147..61c7a25 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -109,6 +109,7 @@
 import {pluralize} from '../../../utils/string-util';
 import {fireAlert, fireEvent, fireServerError} from '../../../utils/event-util';
 import {ErrorCallback} from '../../../api/rest';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -167,8 +168,6 @@
   };
 }
 
-const DEBOUNCER_STORE = 'store';
-
 @customElement('gr-reply-dialog')
 export class GrReplyDialog extends KeyboardShortcutMixin(
   LegacyElementMixin(PolymerElement)
@@ -377,6 +376,8 @@
 
   private readonly jsAPI = appContext.jsApiService;
 
+  private storeTask?: DelayedTask;
+
   get keyBindings() {
     return {
       esc: '_handleEscKey',
@@ -428,7 +429,7 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_STORE);
+    this.storeTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -1338,8 +1339,8 @@
   }
 
   _draftChanged(newDraft: string, oldDraft?: string) {
-    this.debounce(
-      DEBOUNCER_STORE,
+    this.storeTask = debounce(
+      this.storeTask,
       () => {
         if (!newDraft.length && oldDraft) {
           // If the draft has been modified to be empty, then erase the storage
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 466f7bd..8d59bc8 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -801,12 +801,12 @@
     const location = element._getStorageLocation();
 
     element.draft = firstEdit;
-    element.flushDebouncer('store');
+    element.storeTask.flush();
 
     assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit));
 
     element.draft = '';
-    element.flushDebouncer('store');
+    element.storeTask.flush();
 
     assert.isTrue(eraseDraftCommentStub.calledWith(location));
   });
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index 9b7bc69..ae808ba 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -39,6 +39,7 @@
   ShowErrorEvent,
 } from '../../../types/events';
 import {windowLocationReload} from '../../../utils/dom-util';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -74,8 +75,6 @@
   };
 }
 
-const DEBOUNCER_CHECK_LOGGED_IN = 'checkLoggedIn';
-
 @customElement('gr-error-manager')
 export class GrErrorManager extends LegacyElementMixin(PolymerElement) {
   static get template() {
@@ -117,6 +116,8 @@
 
   private readonly restApiService = appContext.restApiService;
 
+  private checkLoggedInTask?: DelayedTask;
+
   /** @override */
   connectedCallback() {
     super.connectedCallback();
@@ -157,7 +158,7 @@
       this.handleVisibilityChange
     );
     document.removeEventListener('show-auth-required', this.handleAuthRequired);
-    this.cancelDebouncer(DEBOUNCER_CHECK_LOGGED_IN);
+    this.checkLoggedInTask?.cancel();
 
     if (this._authErrorHandlerDeregistrationHook) {
       this._authErrorHandlerDeregistrationHook();
@@ -414,9 +415,9 @@
   };
 
   _requestCheckLoggedIn() {
-    this.debounce(
-      DEBOUNCER_CHECK_LOGGED_IN,
-      this._checkSignedIn,
+    this.checkLoggedInTask = debounce(
+      this.checkLoggedInTask,
+      () => this._checkSignedIn(),
       CHECK_SIGN_IN_INTERVAL_MS
     );
   }
@@ -491,7 +492,7 @@
   }
 
   private readonly handleWindowFocus = () => {
-    this.flushDebouncer(DEBOUNCER_CHECK_LOGGED_IN);
+    this.checkLoggedInTask?.flush();
   };
 
   private readonly handleShowErrorDialog = (e: ShowErrorEvent) => {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
index 9ddc628..7d643a0 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
@@ -298,7 +298,7 @@
       // now fake authed
       fetchStub.returns(Promise.resolve({status: 204}));
       element.handleWindowFocus();
-      element.flushDebouncer('checkLoggedIn');
+      element.checkLoggedInTask.flush();
       await flush();
       assert.isTrue(refreshStub.called);
       assert.isTrue(hideToastSpy.called);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
index fb28e2b..5ccb3b3 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -30,6 +30,7 @@
 import {GrDiffBuilderElement} from '../gr-diff-builder/gr-diff-builder-element';
 import {FILE} from '../gr-diff/gr-diff-line';
 import {getRange, getSide} from '../gr-diff/gr-diff-utils';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 interface SidedRange {
   side: Side;
@@ -53,8 +54,6 @@
   rootId: string;
 }
 
-const DEBOUNCER_SELECTION_CHANGE = 'selectionChange';
-
 @customElement('gr-diff-highlight')
 export class GrDiffHighlight extends LegacyElementMixin(PolymerElement) {
   static get template() {
@@ -73,6 +72,8 @@
   @property({type: Object, notify: true})
   selectedRange?: SidedRange;
 
+  private selectionChangeTask?: DelayedTask;
+
   /** @override */
   created() {
     super.created();
@@ -89,7 +90,7 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_SELECTION_CHANGE);
+    this.selectionChangeTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -127,8 +128,8 @@
     // quick 'c' press after the selection change. If you wait less than 10
     // ms, then you will have about 50 _handleSelection calls when doing a
     // simple drag for select.
-    this.debounce(
-      DEBOUNCER_SELECTION_CHANGE,
+    this.selectionChangeTask = debounce(
+      this.selectionChangeTask,
       () => this._handleSelection(selection, isMouseUp),
       10
     );
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index 0bb7e7e..53a2404 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -158,6 +158,7 @@
       element.reload();
       // Multiple cascading microtasks are scheduled.
       await flush();
+      await flush();
       // Reporting can be called with other parameters (ex. PluginsLoaded),
       // but only 'Diff Total Render' is important in this test.
       assert.equal(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
index 178f1aa..4b91ead 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.ts
@@ -32,6 +32,7 @@
 import {customElement, property} from '@polymer/decorators';
 import {DiffContent} from '../../../types/diff';
 import {Side} from '../../../constants/constants';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const WHOLE_FILE = -1;
 
@@ -63,8 +64,6 @@
  */
 const MAX_GROUP_SIZE = 120;
 
-const DEBOUNCER_RESET_IS_SCROLLING = 'resetIsScrolling';
-
 /**
  * Converts the API's `DiffContent`s  to `GrDiffGroup`s for rendering.
  *
@@ -113,6 +112,8 @@
   @property({type: Boolean})
   _isScrolling?: boolean;
 
+  private resetIsScrollingTask?: DelayedTask;
+
   /** @override */
   connectedCallback() {
     super.connectedCallback();
@@ -121,7 +122,7 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_RESET_IS_SCROLLING);
+    this.resetIsScrollingTask?.cancel();
     this.cancel();
     window.removeEventListener('scroll', this.handleWindowScroll);
     super.disconnectedCallback();
@@ -129,11 +130,9 @@
 
   private readonly handleWindowScroll = () => {
     this._isScrolling = true;
-    this.debounce(
-      DEBOUNCER_RESET_IS_SCROLLING,
-      () => {
-        this._isScrolling = false;
-      },
+    this.resetIsScrollingTask = debounce(
+      this.resetIsScrollingTask,
+      () => (this._isScrolling = false),
       50
     );
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index d36c1fd..3ee87c0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -75,6 +75,7 @@
 } from '../../../api/diff';
 import {isSafari} from '../../../utils/dom-util';
 import {assertIsDefined} from '../../../utils/common-util';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const NO_NEWLINE_BASE = 'No newline at end of base file.';
 const NO_NEWLINE_REVISION = 'No newline at end of revision file.';
@@ -92,8 +93,6 @@
  */
 const COMMIT_MSG_LINE_LENGTH = 72;
 
-const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
-
 export interface LineOfInterest {
   number: number;
   leftSide: boolean;
@@ -283,6 +282,8 @@
   @property({type: Array})
   layers?: DiffLayer[];
 
+  private renderDiffTableTask?: DelayedTask;
+
   /** @override */
   created() {
     super.created();
@@ -302,7 +303,7 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
+    this.renderDiffTableTask?.cancel();
     this._unobserveIncrementalNodes();
     this._unobserveNodes();
     super.disconnectedCallback();
@@ -475,7 +476,7 @@
   /** Cancel any remaining diff builder rendering work. */
   cancel() {
     this.$.diffBuilder.cancel();
-    this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME);
+    this.renderDiffTableTask?.cancel();
   }
 
   getCursorStops(): Array<HTMLElement | AbortStop> {
@@ -774,7 +775,7 @@
    * render once.
    */
   _debounceRenderDiffTable() {
-    this.debounce(RENDER_DIFF_TABLE_DEBOUNCE_NAME, () =>
+    this.renderDiffTableTask = debounce(this.renderDiffTableTask, () =>
       this._renderDiffTable()
     );
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index c6bd8d6..1827def 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -660,14 +660,14 @@
           change_type: 'MODIFIED',
           content: [{skip: 66}],
         };
-        element.flushDebouncer('renderDiffTable');
+        element.renderDiffTableTask.flush();
       });
 
       test('change in preferences re-renders diff', () => {
         sinon.stub(element, '_renderDiffTable');
         element.prefs = {
           ...MINIMAL_PREFS, time_format: 'HHMM_12'};
-        element.flushDebouncer('renderDiffTable');
+        element.renderDiffTableTask.flush();
         assert.isTrue(element._renderDiffTable.called);
       });
 
@@ -676,14 +676,14 @@
         const newPrefs1 = {...MINIMAL_PREFS,
           line_wrapping: true};
         element.prefs = newPrefs1;
-        element.flushDebouncer('renderDiffTable');
+        element.renderDiffTableTask.flush();
         assert.isTrue(element._renderDiffTable.called);
         stub.reset();
 
         const newPrefs2 = {...newPrefs1};
         delete newPrefs2.line_wrapping;
         element.prefs = newPrefs2;
-        element.flushDebouncer('renderDiffTable');
+        element.renderDiffTableTask.flush();
         assert.isTrue(element._renderDiffTable.called);
       });
 
@@ -693,7 +693,7 @@
         element.noRenderOnPrefsChange = true;
         element.prefs = {
           ...MINIMAL_PREFS, time_format: 'HHMM_12'};
-        element.flushDebouncer('renderDiffTable');
+        element.renderDiffTableTask.flush();
         assert.isFalse(element._renderDiffTable.called);
       });
     });
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 86dd5b6..6b29e85 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -45,6 +45,7 @@
 import {appContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
 import {assertIsDefined} from '../../../utils/common-util';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const SAVING_MESSAGE = 'Saving changes...';
@@ -55,8 +56,6 @@
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
 
-const DEBOUNCER_STORE = 'store';
-
 @customElement('gr-editor-view')
 export class GrEditorView extends KeyboardShortcutMixin(
   LegacyElementMixin(PolymerElement)
@@ -123,6 +122,8 @@
 
   private readonly storage = new GrStorage();
 
+  private storeTask?: DelayedTask;
+
   reporting = appContext.reportingService;
 
   get keyBindings() {
@@ -149,7 +150,7 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_STORE);
+    this.storeTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -355,8 +356,8 @@
   }
 
   _handleContentChange(e: CustomEvent<{value: string}>) {
-    this.debounce(
-      DEBOUNCER_STORE,
+    this.storeTask = debounce(
+      this.storeTask,
       () => {
         const content = e.detail.value;
         if (content) {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
index 8512545..a29d45d 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
@@ -109,7 +109,7 @@
       bubbles: true, composed: true,
       detail: {value: 'new content value'},
     }));
-    element.flushDebouncer('store');
+    element.storeTask.flush();
     flush();
 
     assert.equal(element._newContent, 'new content value');
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index 9993509..a4b0446 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -29,6 +29,7 @@
 import {PaperInputElementExt} from '../../../types/types';
 import {CustomKeyboardEvent} from '../../../types/events';
 import {fireEvent} from '../../../utils/event-util';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
 const DEBOUNCE_WAIT_MS = 200;
@@ -65,8 +66,6 @@
   AutocompleteCommitEventDetail
 >;
 
-const DEBOUNCER_UPDATE_SUGGESTIONS = 'update-suggestions';
-
 @customElement('gr-autocomplete')
 export class GrAutocomplete extends KeyboardShortcutMixin(
   LegacyElementMixin(PolymerElement)
@@ -200,6 +199,8 @@
   @property({type: Object})
   _selected: HTMLElement | null = null;
 
+  private updateSuggestionsTask?: DelayedTask;
+
   get _nativeInput() {
     // In Polymer 2 inputElement isn't nativeInput anymore
     return (this.$.input.$.nativeInput ||
@@ -215,7 +216,7 @@
   /** @override */
   disconnectedCallback() {
     document.removeEventListener('click', this.handleBodyClick);
-    this.cancelDebouncer(DEBOUNCER_UPDATE_SUGGESTIONS);
+    this.updateSuggestionsTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -329,7 +330,11 @@
     if (noDebounce) {
       update();
     } else {
-      this.debounce(DEBOUNCER_UPDATE_SUGGESTIONS, update, DEBOUNCE_WAIT_MS);
+      this.updateSuggestionsTask = debounce(
+        this.updateSuggestionsTask,
+        update,
+        DEBOUNCE_WAIT_MS
+      );
     }
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
index 2dda586..d72007e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.js
@@ -215,19 +215,18 @@
   });
 
   test('noDebounce=false debounces the query', () => {
+    const clock = sinon.useFakeTimers();
     const queryStub = sinon.spy(() => Promise.resolve([]));
-    let callback;
-    const debounceStub = sinon.stub(element, 'debounce').callsFake(
-        (name, cb) => { callback = cb; });
     element.query = queryStub;
     element.noDebounce = false;
     focusOnInput(element);
     element.text = 'a';
+
+    // not called right away
     assert.isFalse(queryStub.called);
-    assert.isTrue(debounceStub.called);
-    assert.equal(debounceStub.lastCall.args[2], 200);
-    assert.isFunction(callback);
-    callback();
+
+    // but called after a while
+    clock.tick(1000);
     assert.isTrue(queryStub.called);
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 2807730..b64f399 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -62,6 +62,7 @@
 import {fireAlert} from '../../../utils/event-util';
 import {pluralize} from '../../../utils/string-util';
 import {assertIsDefined} from '../../../utils/common-util';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const STORAGE_DEBOUNCE_INTERVAL = 400;
 const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -101,12 +102,6 @@
   };
 }
 
-const DEBOUNCER_FIRE_UPDATE = 'fire-update';
-
-const DEBOUNCER_STORE = 'store';
-
-const DEBOUNCER_DRAFT_TOAST = 'draft-toast';
-
 @customElement('gr-comment')
 export class GrComment extends KeyboardShortcutMixin(
   LegacyElementMixin(PolymerElement)
@@ -281,6 +276,12 @@
 
   reporting = appContext.reportingService;
 
+  private fireUpdateTask?: DelayedTask;
+
+  private storeTask?: DelayedTask;
+
+  private draftToastTask?: DelayedTask;
+
   /** @override */
   connectedCallback() {
     super.connectedCallback();
@@ -299,9 +300,9 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_FIRE_UPDATE);
-    this.cancelDebouncer(DEBOUNCER_STORE);
-    this.cancelDebouncer(DEBOUNCER_DRAFT_TOAST);
+    this.fireUpdateTask?.cancel();
+    this.storeTask?.cancel();
+    this.draftToastTask?.cancel();
     if (this.textarea) {
       this.textarea.closeDropdown();
     }
@@ -495,7 +496,7 @@
   _eraseDraftComment() {
     // Prevents a race condition in which removing the draft comment occurs
     // prior to it being saved.
-    this.cancelDebouncer(DEBOUNCER_STORE);
+    this.storeTask?.cancel();
 
     assertIsDefined(this.comment?.path, 'comment.path');
     assertIsDefined(this.changeNum, 'changeNum');
@@ -545,7 +546,7 @@
   }
 
   _fireUpdate() {
-    this.debounce(DEBOUNCER_FIRE_UPDATE, () => {
+    this.fireUpdateTask = debounce(this.fireUpdateTask, () => {
       this.dispatchEvent(
         new CustomEvent('comment-update', {
           detail: this._getEventPayload(),
@@ -653,8 +654,8 @@
       : this._getPatchNum();
     const {path, line, range} = this.comment;
     if (path) {
-      this.debounce(
-        DEBOUNCER_STORE,
+      this.storeTask = debounce(
+        this.storeTask,
         () => {
           const message = this._messageText;
           if (this.changeNum === undefined) {
@@ -736,7 +737,7 @@
   }
 
   _fireDiscard() {
-    this.cancelDebouncer(DEBOUNCER_FIRE_UPDATE);
+    this.fireUpdateTask?.cancel();
     this.dispatchEvent(
       new CustomEvent('comment-discard', {
         detail: this._getEventPayload(),
@@ -859,7 +860,7 @@
 
     // Cancel the debouncer so that error toasts from the error-manager will
     // not be overridden.
-    this.cancelDebouncer(DEBOUNCER_DRAFT_TOAST);
+    this.draftToastTask?.cancel();
     this._updateRequestToast(
       this._numPendingDraftRequests.number,
       /* requestFailed=*/ true
@@ -868,8 +869,8 @@
 
   _updateRequestToast(numPending: number, requestFailed?: boolean) {
     const message = this._getSavingMessage(numPending, requestFailed);
-    this.debounce(
-      DEBOUNCER_DRAFT_TOAST,
+    this.draftToastTask = debounce(
+      this.draftToastTask,
       () => {
         // Note: the event is fired on the body rather than this element because
         // this element may not be attached by the time this executes, in which
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index f28a7fe..b5205a6 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -768,7 +768,7 @@
       });
       MockInteractions.tap(element.shadowRoot
           .querySelector('.cancel'));
-      element.flushDebouncer('fire-update');
+      element.fireUpdateTask.flush();
       element._messageText = '';
       flush();
       MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
@@ -867,21 +867,20 @@
 
     test('draft saving/editing', done => {
       const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
-      const cancelDebounce = sinon.stub(element, 'cancelDebouncer');
 
       element.draft = true;
       flush();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       element._messageText = 'good news, everyone!';
-      element.flushDebouncer('fire-update');
-      element.flushDebouncer('store');
+      element.fireUpdateTask.flush();
+      element.storeTask.flush();
       assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update');
       assert.isTrue(dispatchEventStub.calledTwice);
 
       element._messageText = 'good news, everyone!';
-      element.flushDebouncer('fire-update');
-      element.flushDebouncer('store');
+      element.fireUpdateTask.flush();
+      element.storeTask.flush();
       assert.isTrue(dispatchEventStub.calledTwice);
 
       MockInteractions.tap(element.shadowRoot
@@ -892,7 +891,7 @@
 
       element._xhrPromise.then(draft => {
         assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-save');
-        assert(cancelDebounce.calledWith('store'));
+        assert.isFalse(element.storeTask.isActive());
 
         assert.deepEqual(dispatchEventStub.lastCall.args[0].detail, {
           comment: {
@@ -940,8 +939,8 @@
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       element._messageText = 'good news, everyone!';
-      element.flushDebouncer('fire-update');
-      element.flushDebouncer('store');
+      element.fireUpdateTask.flush();
+      element.storeTask.flush();
 
       element.disabled = true;
       MockInteractions.tap(element.shadowRoot
@@ -1027,7 +1026,7 @@
       const eraseStub = sinon.stub(element.storage, 'eraseDraftComment');
       element._messageText = 'test text';
       flush();
-      element.flushDebouncer('store');
+      element.storeTask.flush();
 
       assert.isTrue(storeStub.called);
       assert.equal(storeStub.lastCall.args[1], 'test text');
@@ -1042,7 +1041,7 @@
       const storeStub = sinon.stub(element.storage, 'setDraftComment');
       element._messageText = 'test text';
       flush();
-      element.flushDebouncer('store');
+      if (element.storeTask) element.storeTask.flush();
 
       assert.isFalse(storeStub.called);
       element._handleCancel({preventDefault: () => {}});
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
index c849fac..06a91ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.ts
@@ -26,6 +26,7 @@
 import {fireAlert, fireEvent} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {KnownExperimentId} from '../../../services/flags/flags';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const RESTORED_MESSAGE = 'Content restored from a previous edit.';
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -36,8 +37,6 @@
   }
 }
 
-const DEBOUNCER_STORE = 'store';
-
 @customElement('gr-editable-content')
 export class GrEditableContent extends LegacyElementMixin(PolymerElement) {
   static get template() {
@@ -119,6 +118,8 @@
 
   private readonly reporting = appContext.reportingService;
 
+  private storeTask?: DelayedTask;
+
   /** @override */
   ready() {
     super.ready();
@@ -129,7 +130,7 @@
 
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_STORE);
+    this.storeTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -149,8 +150,8 @@
     if (!this.storageKey) return;
     const storageKey = this.storageKey;
 
-    this.debounce(
-      DEBOUNCER_STORE,
+    this.storeTask = debounce(
+      this.storeTask,
       () => {
         if (newContent.length) {
           this.storage.setEditableContentItem(storageKey, newContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
index b99b119..7977f4d 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.js
@@ -126,7 +126,7 @@
 
       element._newContent = 'new content';
       flush();
-      element.flushDebouncer('store');
+      element.storeTask.flush();
 
       assert.isTrue(storeStub.called);
       assert.deepEqual(
@@ -135,7 +135,7 @@
 
       element._newContent = '';
       flush();
-      element.flushDebouncer('store');
+      element.storeTask.flush();
 
       assert.isTrue(eraseStub.called);
       assert.deepEqual([element.storageKey], eraseStub.lastCall.args);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
index 8763bbc..b1c695e 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
@@ -16,8 +16,6 @@
  */
 import '../../../styles/shared-styles';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {Debouncer} from '@polymer/polymer/lib/utils/debounce';
-import {timeOut} from '@polymer/polymer/lib/utils/async';
 import {getRootElement} from '../../../scripts/rootElement';
 import {Constructor} from '../../../utils/common-util';
 import {PolymerElement} from '@polymer/polymer/polymer-element';
@@ -29,6 +27,7 @@
   removeScrollLock,
 } from '@polymer/iron-overlay-behavior/iron-scroll-manager';
 import {ShowAlertEventDetail} from '../../../types/events';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 interface ReloadEventDetail {
   clearPatchset?: boolean;
 }
@@ -110,9 +109,9 @@
       @property({type: String})
       containerId = 'gr-hovercard-container';
 
-      private hideDebouncer: Debouncer | null = null;
+      private hideTask?: DelayedTask;
 
-      private showDebouncer: Debouncer | null = null;
+      private showTask?: DelayedTask;
 
       private isScheduledToShow?: boolean;
 
@@ -142,8 +141,8 @@
       }
 
       disconnectedCallback() {
-        this.cancelShowDebouncer();
-        this.cancelHideDebouncer();
+        this.cancelShowTask();
+        this.cancelHideTask();
         this.unlock();
         super.disconnectedCallback();
       }
@@ -173,24 +172,24 @@
       }
 
       readonly debounceHide = () => {
-        this.cancelShowDebouncer();
+        this.cancelShowTask();
         if (!this._isShowing || this.isScheduledToHide) return;
         this.isScheduledToHide = true;
-        this.hideDebouncer = Debouncer.debounce(
-          this.hideDebouncer,
-          timeOut.after(HIDE_DELAY_MS),
+        this.hideTask = debounce(
+          this.hideTask,
           () => {
             // This happens when hide immediately through click or mouse leave
             // on the hovercard
             if (!this.isScheduledToHide) return;
             this.hide();
-          }
+          },
+          HIDE_DELAY_MS
         );
       };
 
-      cancelHideDebouncer() {
-        if (this.hideDebouncer) {
-          this.hideDebouncer.cancel();
+      cancelHideTask() {
+        if (this.hideTask) {
+          this.hideTask.cancel();
           this.isScheduledToHide = false;
         }
       }
@@ -258,8 +257,8 @@
        *
        */
       readonly hide = (e?: MouseEvent) => {
-        this.cancelHideDebouncer();
-        this.cancelShowDebouncer();
+        this.cancelHideTask();
+        this.cancelShowTask();
         if (!this._isShowing) {
           return;
         }
@@ -304,23 +303,23 @@
        * Shows/opens the hovercard with the given delay.
        */
       debounceShowBy(delayMs: number) {
-        this.cancelHideDebouncer();
+        this.cancelHideTask();
         if (this._isShowing || this.isScheduledToShow) return;
         this.isScheduledToShow = true;
-        this.showDebouncer = Debouncer.debounce(
-          this.showDebouncer,
-          timeOut.after(delayMs),
+        this.showTask = debounce(
+          this.showTask,
           () => {
             // This happens when the mouse leaves the target before the delay is over.
             if (!this.isScheduledToShow) return;
             this.show();
-          }
+          },
+          delayMs
         );
       }
 
-      cancelShowDebouncer() {
-        if (this.showDebouncer) {
-          this.showDebouncer.cancel();
+      cancelShowTask() {
+        if (this.showTask) {
+          this.showTask.cancel();
           this.isScheduledToShow = false;
         }
       }
@@ -337,8 +336,8 @@
        * `mousenter` event on the hovercard's `target` element.
        */
       readonly show = () => {
-        this.cancelHideDebouncer();
-        this.cancelShowDebouncer();
+        this.cancelHideTask();
+        this.cancelShowTask();
         if (this._isShowing || !this.container) {
           return;
         }
@@ -483,12 +482,12 @@
   ready(): void;
   removeListeners(): void;
   debounceHide(): void;
-  cancelHideDebouncer(): void;
+  cancelHideTask(): void;
   dispatchEventThroughTarget(eventName: string, detail?: unknown): void;
   hide(e?: MouseEvent): void;
   debounceShow(): void;
   debounceShowBy(delayMs: number): void;
-  cancelShowDebouncer(): void;
+  cancelShowTask(): void;
   show(): void;
   updatePosition(): void;
   updatePositionTo(position: string): void;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
index 628b1e9..27ef23f 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.js
@@ -121,7 +121,7 @@
 
     await enterPromise;
     assert.isTrue(element.isScheduledToShow);
-    element.showDebouncer.flush();
+    element.showTask.flush();
     assert.isTrue(element._isShowing);
     assert.isFalse(element.isScheduledToShow);
 
@@ -130,7 +130,7 @@
     await leavePromise;
     assert.isTrue(element.isScheduledToHide);
     assert.isTrue(element._isShowing);
-    element.hideDebouncer.flush();
+    element.hideTask.flush();
     assert.isFalse(element.isScheduledToShow);
     assert.isFalse(element._isShowing);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
index 71b8bc7..86a2d1a 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.ts
@@ -25,6 +25,7 @@
 import {page} from '../../../utils/page-wrapper-utils';
 import {property, customElement} from '@polymer/decorators';
 import {fireEvent} from '../../../utils/event-util';
+import {debounce, DelayedTask} from '../../../utils/async-util';
 
 const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
 
@@ -34,8 +35,6 @@
   }
 }
 
-const DEBOUNCER_RELOAD = 'reload';
-
 @customElement('gr-list-view')
 class GrListView extends LegacyElementMixin(PolymerElement) {
   static get template() {
@@ -63,9 +62,11 @@
   @property({type: String})
   path?: string;
 
+  private reloadTask?: DelayedTask;
+
   /** @override */
   disconnectedCallback() {
-    this.cancelDebouncer(DEBOUNCER_RELOAD);
+    this.reloadTask?.cancel();
     super.disconnectedCallback();
   }
 
@@ -79,8 +80,8 @@
   }
 
   _debounceReload(filter?: string) {
-    this.debounce(
-      DEBOUNCER_RELOAD,
+    this.reloadTask = debounce(
+      this.reloadTask,
       () => {
         if (this.path) {
           if (filter) {
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 4e1662c..848eeaf 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -32,7 +32,6 @@
   removeIronOverlayBackdropStyleEl,
   TestKeyboardShortcutBinder,
 } from './test-utils';
-import {flushDebouncers} from '@polymer/polymer/lib/utils/debounce';
 import {_testOnly_getShortcutManagerInstance} from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import sinon, {SinonSpy} from 'sinon/pkg/sinon-esm';
 import {safeTypesBridge} from '../utils/safe-types-util';
@@ -43,6 +42,7 @@
   _testOnly_defaultResinReportHandler,
   installPolymerResin,
 } from '../scripts/polymer-resin-install';
+import {_testOnly_allTasks} from '../utils/async-util';
 
 declare global {
   interface Window {
@@ -190,19 +190,20 @@
   }
 }
 
+function cancelAllTasks() {
+  for (const task of _testOnly_allTasks.values()) {
+    console.warn('ATTENTION! A task was still active at the end of the test!');
+    task.cancel();
+  }
+}
+
 teardown(() => {
   sinon.restore();
   cleanupTestUtils();
   TestKeyboardShortcutBinder.pop();
   checkGlobalSpace();
   removeIronOverlayBackdropStyleEl();
-  // Clean Polymer debouncer queue, so next tests will not be affected.
-  // WARNING! This will most likely not do what you expect. `flushDebouncers()`
-  // will only flush debouncers that were added using `enqueueDebouncer()`. So
-  // this will not affect "normal" debouncers that were added using
-  // `this.debounce()`. For those please be careful and cancel them using
-  // `this.cancelDebouncer()` in the `detached()` lifecycle hook.
-  flushDebouncers();
+  cancelAllTasks();
   const testTeardownTimestampMs = new Date().getTime();
   const elapsedMs = testTeardownTimestampMs - testSetupTimestampMs;
   if (elapsedMs > 1000) {
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index 119b09b..2b36fee 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -42,3 +42,71 @@
     return asyncForeach(array.slice(1), fn);
   });
 }
+
+export const _testOnly_allTasks = new Map<number, DelayedTask>();
+
+/**
+ * This is just a very simple and small wrapper around setTimeout(). Instead of
+ * the usual:
+ *
+ * const timer = window.setTimeout(() => {...do stuff...}, 123);
+ * window.clearTimeout(timer);
+ *
+ * With this class you can do:
+ *
+ * const task = new Task(() => {...do stuff...}, 123);
+ * task.cancel();
+ *
+ * It is just nicer to have an object for this instead of a number as a handle.
+ */
+export class DelayedTask {
+  private timer?: number;
+
+  constructor(private callback: () => void, waitMs = 0) {
+    this.timer = window.setTimeout(() => {
+      if (this.timer) _testOnly_allTasks.delete(this.timer);
+      this.timer = undefined;
+      if (this.callback) this.callback();
+    }, waitMs);
+    _testOnly_allTasks.set(this.timer, this);
+  }
+
+  cancel() {
+    if (this.isActive()) {
+      window.clearTimeout(this.timer);
+      if (this.timer) _testOnly_allTasks.delete(this.timer);
+      this.timer = undefined;
+    }
+  }
+
+  flush() {
+    if (this.isActive()) {
+      this.cancel();
+      if (this.callback) this.callback();
+    }
+  }
+
+  isActive() {
+    return this.timer !== undefined;
+  }
+}
+
+/**
+ * The usage pattern is:
+ *
+ * this.myDebouncedTask = debounce(this.myDebouncedTask, () => {...}, 123);
+ *
+ * It is identical to:
+ *
+ * this.myTask = new DelayedTask(() => {...}, 123);
+ *
+ * But it would cancel a potentially scheduled task beforehand.
+ */
+export function debounce(
+  existingTask: DelayedTask | undefined,
+  callback: () => void,
+  waitMs = 0
+) {
+  existingTask?.cancel();
+  return new DelayedTask(callback, waitMs);
+}