WTI UI Scoreboard Implementation - pc2ccs/pc2v9 GitHub Wiki

Overview

The PC² Web Team Interface (consisting of the WTI-API and WTI-UI projects) did not originally include a scoreboard (that is, there was no way in the original WTI project for teams to view the current contest standings). A subsequent effort added a Scoreboard link to the WTI-UI team interface, along with adding support for obtaining the current contest standings via the WTI-API.

When a team first makes a browser connection to the WTI-API server, the browser effectively downloads an Angular 7 single page application -- the WTI-UI project (see WTI UI Angular Project Structure for details on Angular and the overall structure of the WTI-UI Angular implementation). When this application runs in the browser it starts at an Angular module named AppModule. This module initializes the application within the browser and then uses a module named LoginModule to display an Angular component named LoginPageComponent -- the "login page".

Logging in with valid credentials establishes a connection to the WTI-API server (which in turn establishes a connection through the WTI-API server to the PC² server). Logging in also establishes a websocket connection between the browser (client) and the WTI-API server, as well as registering a websocket listener (an instance of WTI-UI Angular class WebsocketService) listening for messages on the websocket from the WTI-API server.

The WTI-UI project contains an Angular module named AppHeaderComponent which, together with its HTML template file app-header.component.html, defines the "links" displayed at the top of the WTI-UI screen. The WTI-UI Angular module AppRoutingModule arranges that (after a successful login) when the "Scoreboard" link is clicked, control is transferred to an Angular module named ScoreboardModule.

ScoreboardModule declares a single component named ScoreboardPageComponent, which is defined as a Typescript class in file app/modules/scoreboard/components/scoreboard-page.component.ts. Like all Angular components, ScoreboardPageComponent also has a corresponding HTML template file scoreboard-page.component.html, which defines the HTML structure of the scoreboard page.

The ScoreboardPageComponent class is injected with (that is, has access to) an implementation of WTI-UI class IContestService, and it implements Angular interfaces OnInit, OnDestroy, and DoCheck. The OnInit interface method (which is named ngOnInit, following Angular conventions of prepending ng to all interface method names) invokes local method loadStandings(), which in turn invokes the injected IContestService's getStandings() method, which returns a JSON string containing the current contest standings.

loadStandings() then converts the JSON standings into an array of rows, each representing the scoreboard standing for a single team. This array, called teamStandings, is iterated over by an *ngFordirective in scoreboard-page.component.html, which causes each row of the standings to appear in the HTML page displayed by ScoreboardPageComponent.

Standings Caching

The WTI-API specification includes an endpoint /api/contest/scoreboard on which clients can make HTTP requests to obtain current scoreboard standings (see WTI API Scoreboard Implementation for details on the /scoreboard API endpoint). When the WTI-UI IContestService's getStandings() method is first invoked, it issues an HTTP request to this WTI-API /scoreboard endpoint, receiving back the current contest standings -- which it returns to the WTI-UI ScoreboardPageComponent.

The WTI-UI IContestService implementation maintains a cached copy of the most recent contest standings which have been fetched from the WTI-API server, together with a flag indicating whether the standings are still "current". Whenever the IContestService getStandings() method is called, it checks this flag to see if there has been any indication from the WTI-API server that the standings are out of date. If not, it simply returns the cached copy. If the cached standings have been flagged as "out-of-date", getStandings() makes a new HTTP request to the WTI-API server for updated standings.

As mentioned above, starting the WTI-UI application establishes a websocket between the browser and the WTI-API server. Whenever the WTI-API server detects a change in contest status that could affect scoreboard standings (for example, a submission being judged, a change in a contest configuration item which affects scoring, etc.) it sends a "standings" message through the websocket to the browser (see WTI-API Scoreboard Implementation for details on how this is implemented on the WTI-API server side). This message is processed by the registered Angular WTI-UI websocket listener, which results in setting the flag in the IContestService module to indicate that the cached standings are "out of date". The result is that the next time a WTI-UI component asks the IContestService for standings, new (updated) standings will be retrieved from the WTI-API server.

Dynamic Scoreboard Updates

The net effect of the above is that when a team clicks on the Scoreboard link the WTI-UI browser application code creates a new ScoreboardPage component and switches to displaying that component, which obtains the standings (either a cached copy or an updated copy) from the IContestService as part of the ScoreboardPage's initialization. That is, switching to the Scoreboard generates a new ScoreboardPage component, and the standings displayed on that ScoreboardPage component are automatically updated (by the Angular ngOnInit method) each time the user switches to the Scoreboard.

However, this mechanism does not provide support for dynamically-updating scoreboards. That is, if a team simply leaves the scoreboard page displaying, the above mechanism will not cause the scoreboard to update; it requires reloading (switching away from and then back to) the scoreboard page to get updated standings.

The workaround for this is to augment the ScoreboardPageComponent so that it implements the Angular DoCheck interface. This provides a hook, named ngDoCheck(), which Angular automatically invokes once each cycle of the JavaScript engine for each component visible in the current display. The ScoreboardPageComponent's ngDoCheck() method checks the IContestService's "standings are current" flag, and if there has been an event which has marked the client-side cache "not current", the standings are refreshed by invoking loadStandings(), which in turn invokes the IContestService getStandings() method (which will in turn update the standings by issuing an HTTP "get standings" request since it will note that the standings are marked "not current"). Updated standings are then returned to the ScoreboardPageComponent and the scoreboard page is updated; in this way scoreboard standings are dynamically updated any time the WTI-API server indicates they (may have) changed.

Note that this approach (invoking ngDoCheck() repeatedly) is relatively lightweight because (1) it only updates the standings when/if they have changed, and (2) it only occurs when the ScoreboardPage component is being viewed. This is because of a characteristic of the way Angular "routing" works: routing to a component creates a new instance of that component, while routing away from a component destroys that component. (This can be seen by uncommenting the console logging messages in the ScoreboardPageComponent's ngOnInit and ngOnDestroy methods, activating the browser console, and then navigating to and away from the Scoreboard.) Thus, the only time Angular actually invokes the ScoreboardPage component's ngDoCheck() method is when the scoreboard is being viewed -- which is exactly when dynamic updates should be occurring.

Additional Notes

  • The ScoreboardPageComponent assumes that the JSON standings returned by the IContestService's getStandings() method are already in sorted order. That is, ScoreboardPageComponent simply displays the team standings rows in the order they appear in the JSON string received from getStandings(). While the current implementation returns those rows in "standings order" (1st place, then 2nd place, etc.), arguably ScoreboardPageComponent should apply a sorting filter to the resulting array prior to allowing *ngFor to render them on the HTML page.

See Also