Create a service to fetch the match list.

I plan to reuse the functionality for creating a notes scouting
schedule.

Change-Id: Ia449a6567ec63f5faca1af6e4a154cb77c359c88
Signed-off-by: Alex Perry <alex.perry96@gmail.com>
diff --git a/scouting/www/match_list/BUILD b/scouting/www/match_list/BUILD
index a33caf8..10c0a22 100644
--- a/scouting/www/match_list/BUILD
+++ b/scouting/www/match_list/BUILD
@@ -19,6 +19,7 @@
         "//scouting/webserver/requests/messages:error_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
         "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+        "//scouting/www/rpc",
         "@com_github_google_flatbuffers//ts:flatbuffers_ts",
         "@npm//@angular/common",
         "@npm//@angular/core",
diff --git a/scouting/www/match_list/match_list.component.ts b/scouting/www/match_list/match_list.component.ts
index b2a2731..e311664 100644
--- a/scouting/www/match_list/match_list.component.ts
+++ b/scouting/www/match_list/match_list.component.ts
@@ -1,5 +1,5 @@
 import {Component, EventEmitter, OnInit, Output} from '@angular/core';
-import {ByteBuffer, Builder} from 'flatbuffers';
+import {Builder, ByteBuffer} from 'flatbuffers';
 import {ErrorResponse} from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
 import {RequestAllMatches} from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_generated';
 import {
@@ -7,14 +7,14 @@
   RequestAllMatchesResponse,
 } from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_response_generated';
 
+import {MatchListRequestor} from '../rpc/match_list_requestor';
+
 type TeamInMatch = {
   teamNumber: number;
   matchNumber: number;
   compLevel: string;
 };
 
-const MATCH_TYPE_ORDERING = ['qm', 'ef', 'qf', 'sf', 'f'];
-
 @Component({
   selector: 'app-match-list',
   templateUrl: './match_list.ng.html',
@@ -26,6 +26,8 @@
   errorMessage: string = '';
   matchList: Match[] = [];
 
+  constructor(private readonly matchListRequestor: MatchListRequestor) {}
+
   setTeamInMatch(teamInMatch: TeamInMatch) {
     this.selectedTeamEvent.emit(teamInMatch);
   }
@@ -67,62 +69,15 @@
   }
 
   async fetchMatchList() {
+    this.progressMessage = 'Fetching match list. Please be patient.';
     this.errorMessage = '';
 
-    const builder = new Builder();
-    RequestAllMatches.startRequestAllMatches(builder);
-    builder.finish(RequestAllMatches.endRequestAllMatches(builder));
-
-    this.progressMessage = 'Fetching match list. Please be patient.';
-
-    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.
-      this.matchList = [];
-      for (let i = 0; i < parsedResponse.matchListLength(); i++) {
-        this.matchList.push(parsedResponse.matchList(i));
-      }
-
-      // Sort the list so it is in chronological order.
-      this.matchList.sort((a, b) => {
-        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;
-        }
-        const aMatchNumber = a.matchNumber();
-        const bMatchNumber = b.matchNumber();
-        if (aMatchNumber < bMatchNumber) {
-          return -1;
-        }
-        if (aMatchNumber > bMatchNumber) {
-          return 1;
-        }
-        return 0;
-      });
-
+    try {
+      this.matchList = await this.matchListRequestor.fetchMatchList();
       this.progressMessage = 'Successfully fetched match list.';
-    } else {
+    } catch (e) {
+      this.errorMessage = e;
       this.progressMessage = '';
-      const resBuffer = await res.arrayBuffer();
-      const fbBuffer = new ByteBuffer(new Uint8Array(resBuffer));
-      const parsedResponse = ErrorResponse.getRootAsErrorResponse(fbBuffer);
-
-      const errorMessage = parsedResponse.errorMessage();
-      this.errorMessage = `Received ${res.status} ${res.statusText}: "${errorMessage}"`;
     }
   }
 }
diff --git a/scouting/www/rpc/BUILD b/scouting/www/rpc/BUILD
new file mode 100644
index 0000000..581a7b8
--- /dev/null
+++ b/scouting/www/rpc/BUILD
@@ -0,0 +1,19 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+    name = "rpc",
+    srcs = [
+        "match_list_requestor.ts",
+    ],
+    compiler = "//tools:tsc_wrapped_with_angular",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    use_angular_plugin = True,
+    visibility = ["//visibility:public"],
+    deps = [
+        "//scouting/webserver/requests/messages:error_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+        "@npm//@angular/core",
+    ],
+)
diff --git a/scouting/www/rpc/match_list_requestor.ts b/scouting/www/rpc/match_list_requestor.ts
new file mode 100644
index 0000000..917a9b3
--- /dev/null
+++ b/scouting/www/rpc/match_list_requestor.ts
@@ -0,0 +1,68 @@
+import {Injectable} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {ErrorResponse} from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+import {RequestAllMatches} from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_generated';
+import {
+  Match,
+  RequestAllMatchesResponse,
+} from 'org_frc971/scouting/webserver/requests/messages/request_all_matches_response_generated';
+
+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) => {
+        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;
+        }
+        const aMatchNumber = a.matchNumber();
+        const bMatchNumber = b.matchNumber();
+        if (aMatchNumber < bMatchNumber) {
+          return -1;
+        }
+        if (aMatchNumber > bMatchNumber) {
+          return 1;
+        }
+        return 0;
+      });
+
+      return matchList;
+    } else {
+      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}"`;
+    }
+  }
+}