ui: Backport fix for Track outliving its state

Bug: 196965537
Change-Id: Ia83cbec491a5cac69f3858f2468ecff33157115b
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 5ad8bdf..30a0d4c 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import * as m from 'mithril';
+import {assertExists} from '../base/logging';
 import {TrackState} from '../common/state';
 import {TrackData} from '../common/track_data';
 import {checkerboard} from './checkerboard';
@@ -54,19 +55,37 @@
  * The abstract class that needs to be implemented by all tracks.
  */
 export abstract class Track<Config = {}, Data extends TrackData = TrackData> {
+  // The UI-generated track ID (not to be confused with the SQL track.id).
   private trackId: string;
+
+  // Caches the last state.track[this.trackId]. This is to deal with track
+  // deletion, see comments in trackState() below.
+  private lastTrackState: TrackState;
+
   constructor(trackId: string) {
     this.trackId = trackId;
+    this.lastTrackState = assertExists(globals.state.tracks[this.trackId]);
   }
 
   protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
 
   protected get trackState(): TrackState {
-    return globals.state.tracks[this.trackId];
+    // We can end up in a state where a Track is still in the mithril renderer
+    // tree but its corresponding state has been deleted. This can happen in the
+    // interval of time between a track being removed from the state and the
+    // next animation frame that would remove the Track object. If a mouse event
+    // is dispatched in the meanwhile (or a promise is resolved), we need to be
+    // able to access the state. Hence the caching logic here.
+    const trackState = globals.state.tracks[this.trackId];
+    if (trackState === undefined) {
+      return this.lastTrackState;
+    }
+    this.lastTrackState = trackState;
+    return trackState;
   }
 
   get config(): Config {
-    return globals.state.tracks[this.trackId].config as Config;
+    return this.trackState.config as Config;
   }
 
   data(): Data|undefined {