ui: switch pinned tracks save/restore feature to use Zod

Change-Id: I126a9ef58a32e65b0759cadc61620906500a8d5d
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
index b20ecef..fadb4ed 100644
--- a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -16,10 +16,13 @@
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
 import {TrackDescriptor} from '../../public/track';
+import {z} from 'zod';
 
 const PLUGIN_ID = 'dev.perfetto.RestorePinnedTrack';
 const SAVED_TRACKS_KEY = `${PLUGIN_ID}#savedPerfettoTracks`;
 
+const RESTORE_COMMAND_ID = `${PLUGIN_ID}#restore`;
+
 /**
  * Fuzzy save and restore of pinned tracks.
  *
@@ -33,6 +36,7 @@
 
   async onTraceLoad(ctx: Trace): Promise<void> {
     this.ctx = ctx;
+
     ctx.commands.registerCommand({
       id: `${PLUGIN_ID}#save`,
       name: 'Save: Pinned tracks',
@@ -41,7 +45,7 @@
       },
     });
     ctx.commands.registerCommand({
-      id: `${PLUGIN_ID}#restore`,
+      id: RESTORE_COMMAND_ID,
       name: 'Restore: Pinned tracks',
       callback: () => {
         this.restoreTracks();
@@ -50,14 +54,10 @@
   }
 
   private saveTracks() {
-    const workspace = this.ctx.workspace;
-    const pinnedTracks = workspace.pinnedTracks;
-    const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((track) =>
-      this.toSavedTrack(track),
-    );
-
     this.savedState = {
-      tracks: tracksToSave,
+      tracks: this.ctx.workspace.pinnedTracks.map((track) =>
+        this.toSavedTrack(track),
+      ),
     };
   }
 
@@ -67,7 +67,6 @@
       alert('No saved tracks. Use the Save command first');
       return;
     }
-    const tracksToRestore: SavedPinnedTrack[] = savedState.tracks;
 
     const localTracks: Array<LocalTrack> = this.ctx.workspace.flatTracks.map(
       (track) => ({
@@ -75,8 +74,7 @@
         track: track,
       }),
     );
-
-    tracksToRestore.forEach((trackToRestore) => {
+    savedState.tracks.forEach((trackToRestore) => {
       const foundTrack = this.findMatchingTrack(localTracks, trackToRestore);
       if (foundTrack) {
         foundTrack.pin();
@@ -92,8 +90,8 @@
   private findMatchingTrack(
     localTracks: Array<LocalTrack>,
     savedTrack: SavedPinnedTrack,
-  ): TrackNode | null {
-    let mostSimilarTrack: LocalTrack | null = null;
+  ): TrackNode | undefined {
+    let mostSimilarTrack: LocalTrack | undefined = undefined;
     let mostSimilarTrackDifferenceScore: number = 0;
 
     for (let i = 0; i < localTracks.length; i++) {
@@ -119,7 +117,7 @@
       }
     }
 
-    return mostSimilarTrack?.track || null;
+    return mostSimilarTrack?.track || undefined;
   }
 
   /**
@@ -198,7 +196,7 @@
 
   private toSavedTrack(track: TrackNode): SavedPinnedTrack {
     let trackDescriptor: TrackDescriptor | undefined = undefined;
-    if (track.uri != null) {
+    if (track.uri != undefined) {
       trackDescriptor = this.ctx.tracks.getTrack(track.uri);
     }
 
@@ -211,18 +209,18 @@
     };
   }
 
-  private get savedState(): SavedState | null {
+  private get savedState(): SavedState | undefined {
     const savedStateString = window.localStorage.getItem(SAVED_TRACKS_KEY);
     if (!savedStateString) {
-      return null;
+      return undefined;
     }
-
-    const savedState: SavedState = JSON.parse(savedStateString);
-    if (!(savedState.tracks instanceof Array)) {
-      return null;
+    const savedState = SAVED_STATE_SCHEMA.safeParse(
+      JSON.parse(savedStateString),
+    );
+    if (!savedState.success) {
+      return undefined;
     }
-
-    return savedState;
+    return savedState.data;
   }
 
   private set savedState(state: SavedState) {
@@ -240,28 +238,30 @@
   return undefined;
 }
 
-interface SavedState {
-  tracks: Array<SavedPinnedTrack>;
-}
+const SAVED_PINNED_TRACK_SCHEMA = z
+  .object({
+    // Optional: group name for the track. Usually matches with process name.
+    groupName: z.string().optional(),
+    // Track name to restore.
+    trackName: z.string(),
+    // Plugin used to create this track
+    pluginId: z.string().optional(),
+    // Kind of the track
+    kind: z.string().optional(),
+    // If it's a thread track, it should be true in case it's a main thread track
+    isMainThread: z.boolean(),
+  })
+  .readonly();
 
-interface SavedPinnedTrack {
-  // Optional: group name for the track. Usually matches with process name.
-  groupName?: string;
+type SavedPinnedTrack = z.infer<typeof SAVED_PINNED_TRACK_SCHEMA>;
 
-  // Track name to restore.
-  trackName: string;
+const SAVED_STATE_SCHEMA = z
+  .object({tracks: z.array(SAVED_PINNED_TRACK_SCHEMA).readonly()})
+  .readonly();
 
-  // Plugin used to create this track
-  pluginId?: string;
-
-  // Kind of the track
-  kind?: string;
-
-  // If it's a thread track, it should be true in case it's a main thread track
-  isMainThread: boolean;
-}
+type SavedState = z.infer<typeof SAVED_STATE_SCHEMA>;
 
 interface LocalTrack {
-  savedTrack: SavedPinnedTrack;
-  track: TrackNode;
+  readonly savedTrack: SavedPinnedTrack;
+  readonly track: TrackNode;
 }