Scouting: Add ability to scout offline

Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com>
Change-Id: I0e0c0be033824c05b6c239d38eb79d95745fceec
diff --git a/scouting/www/rpc/BUILD b/scouting/www/rpc/BUILD
index d1367ea..592735c 100644
--- a/scouting/www/rpc/BUILD
+++ b/scouting/www/rpc/BUILD
@@ -10,6 +10,7 @@
     ],
     generate_public_api = False,
     deps = [
+        "//:node_modules/dexie",
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_2024_data_scouting_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_2024_data_scouting_ts_fbs",
diff --git a/scouting/www/rpc/db.ts b/scouting/www/rpc/db.ts
new file mode 100644
index 0000000..789ac0e
--- /dev/null
+++ b/scouting/www/rpc/db.ts
@@ -0,0 +1,18 @@
+import Dexie, {Table} from 'dexie';
+
+export interface MatchListData {
+  id?: number;
+  data: Uint8Array;
+}
+
+export class AppDB extends Dexie {
+  matchListData!: Table<MatchListData, number>;
+
+  constructor() {
+    super('ngdexieliveQuery');
+    this.version(1).stores({
+      matchListData: 'id,data',
+    });
+  }
+}
+export const db = new AppDB();
diff --git a/scouting/www/rpc/match_list_requestor.ts b/scouting/www/rpc/match_list_requestor.ts
index fa2dcbd..0a812ce 100644
--- a/scouting/www/rpc/match_list_requestor.ts
+++ b/scouting/www/rpc/match_list_requestor.ts
@@ -6,78 +6,82 @@
   Match,
   RequestAllMatchesResponse,
 } from '../../webserver/requests/messages/request_all_matches_response_generated';
-
+import {db, MatchListData} from './db';
 const MATCH_TYPE_ORDERING = ['qm', 'ef', 'qf', 'sf', 'f'];
-
 @Injectable({providedIn: 'root'})
 export class MatchListRequestor {
   async fetchMatchList(): Promise<Match[]> {
     const builder = new Builder();
     RequestAllMatches.startRequestAllMatches(builder);
     builder.finish(RequestAllMatches.endRequestAllMatches(builder));
-
     const buffer = builder.asUint8Array();
     const res = await fetch('/requests/request/all_matches', {
       method: 'POST',
       body: buffer,
     });
-
     if (res.ok) {
       const resBuffer = await res.arrayBuffer();
-      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-      const parsedResponse =
-        RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(fbBuffer);
-
-      // Convert the flatbuffer list into an array. That's more useful.
-      const matchList = [];
-      for (let i = 0; i < parsedResponse.matchListLength(); i++) {
-        matchList.push(parsedResponse.matchList(i));
-      }
-
-      // Sort the list so it is in chronological order.
-      matchList.sort((a, b) => {
-        // First sort by match type. E.g. finals are last.
-        const aMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(a.compLevel());
-        const bMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(b.compLevel());
-        if (aMatchTypeIndex < bMatchTypeIndex) {
-          return -1;
-        }
-        if (aMatchTypeIndex > bMatchTypeIndex) {
-          return 1;
-        }
-        // Then sort by match number. E.g. in semi finals, all match 1 rounds
-        // are done first. Then come match 2 rounds. And then, if necessary,
-        // the match 3 rounds.
-        const aMatchNumber = a.matchNumber();
-        const bMatchNumber = b.matchNumber();
-        if (aMatchNumber < bMatchNumber) {
-          return -1;
-        }
-        if (aMatchNumber > bMatchNumber) {
-          return 1;
-        }
-        // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
-        // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
-        // Semi Final 2 Match 2.
-        const aSetNumber = a.setNumber();
-        const bSetNumber = b.setNumber();
-        if (aSetNumber < bSetNumber) {
-          return -1;
-        }
-        if (aSetNumber > bSetNumber) {
-          return 1;
-        }
-        return 0;
-      });
-
-      return matchList;
+      const u8Buffer = new Uint8Array(resBuffer);
+      // Cache the response.
+      await db.matchListData.put({id: 1, data: u8Buffer});
+      return this.parseMatchList(u8Buffer);
     } else {
+      const cachedResult = await db.matchListData.where({id: 1}).toArray();
+      if (cachedResult && cachedResult.length == 1) {
+        const u8Buffer = cachedResult[0].data;
+        return this.parseMatchList(u8Buffer);
+      }
       const resBuffer = await res.arrayBuffer();
       const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
       const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
       const errorMessage = parsedResponse.errorMessage();
       throw `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
     }
   }
+  parseMatchList(u8Buffer: Uint8Array): Match[] {
+    const fbBuffer = new ByteBuffer(u8Buffer);
+    const parsedResponse =
+      RequestAllMatchesResponse.getRootAsRequestAllMatchesResponse(fbBuffer);
+    // Convert the flatbuffer list into an array. That's more useful.
+    const matchList = [];
+    for (let i = 0; i < parsedResponse.matchListLength(); i++) {
+      matchList.push(parsedResponse.matchList(i));
+    }
+    // Sort the list so it is in chronological order.
+    matchList.sort((a, b) => {
+      // First sort by match type. E.g. finals are last.
+      const aMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(a.compLevel());
+      const bMatchTypeIndex = MATCH_TYPE_ORDERING.indexOf(b.compLevel());
+      if (aMatchTypeIndex < bMatchTypeIndex) {
+        return -1;
+      }
+      if (aMatchTypeIndex > bMatchTypeIndex) {
+        return 1;
+      }
+      // Then sort by match number. E.g. in semi finals, all match 1 rounds
+      // are done first. Then come match 2 rounds. And then, if necessary,
+      // the match 3 rounds.
+      const aMatchNumber = a.matchNumber();
+      const bMatchNumber = b.matchNumber();
+      if (aMatchNumber < bMatchNumber) {
+        return -1;
+      }
+      if (aMatchNumber > bMatchNumber) {
+        return 1;
+      }
+      // Lastly, sort by set number. I.e. Semi Final 1 Match 1 happens first.
+      // Then comes Semi Final 2 Match 1. Then comes Semi Final 1 Match 2. Then
+      // Semi Final 2 Match 2.
+      const aSetNumber = a.setNumber();
+      const bSetNumber = b.setNumber();
+      if (aSetNumber < bSetNumber) {
+        return -1;
+      }
+      if (aSetNumber > bSetNumber) {
+        return 1;
+      }
+      return 0;
+    });
+    return matchList;
+  }
 }