Browse Source

Tidy up SSEs for better routing

new_auth
Quildra 2 years ago
parent
commit
019f88110d
  1. 14
      package-lock.json
  2. 21
      packages/bridge-server/src/app.controller.ts
  3. 19
      packages/bridge-server/src/race-results/race-results.controller.ts
  4. 4
      packages/bridge-server/src/race-results/race-results.module.ts
  5. 13
      packages/bridge-server/src/season-standings/season-standings.service.ts
  6. 3
      packages/bridge-server/src/season-standings/seasons-standings-updated.event.ts
  7. 2
      packages/bridge-server/src/seasons/seasons.service.ts
  8. 4
      packages/bridge-server/src/sse/sse.module.ts
  9. 39
      packages/bridge-server/src/sse/sse.service.ts
  10. 3
      packages/bridge-ui/package.json
  11. 32
      packages/bridge-ui/src/app/components/race-details/race-details.component.ts
  12. 4
      packages/bridge-ui/src/app/components/season-card/season-card.component.ts
  13. 115
      packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts
  14. 16
      packages/bridge-ui/src/app/services/server-side-events.service.spec.ts
  15. 45
      packages/bridge-ui/src/app/services/server-side-events.service.ts
  16. 3
      packages/bridge-ui/tsconfig.json

14
package-lock.json

@ -7492,9 +7492,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.9.2", "version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -23522,6 +23522,7 @@
"@angular/platform-browser-dynamic": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0", "@angular/router": "^17.0.0",
"bridge-shared": "^1.0.0", "bridge-shared": "^1.0.0",
"eventemitter3": "^5.0.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@ -23532,7 +23533,7 @@
"@angular/cli": "^17.0.1", "@angular/cli": "^17.0.1",
"@angular/compiler-cli": "^17.0.0", "@angular/compiler-cli": "^17.0.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^20.9.2", "@types/node": "^20.10.0",
"jasmine-core": "~5.1.0", "jasmine-core": "~5.1.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
@ -23541,6 +23542,11 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.2.2" "typescript": "~5.2.2"
} }
},
"packages/bridge-ui/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
} }
} }
} }

21
packages/bridge-server/src/app.controller.ts

@ -1,12 +1,29 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get, Req, Res } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { Request, Response } from 'express';
import { SseService } from './sse/sse.service';
@Controller() @Controller()
export class AppController { export class AppController {
constructor(private readonly appService: AppService) {} constructor(
private readonly appService: AppService,
private sseService: SseService
)
{}
@Get() @Get()
getHello(): string { getHello(): string {
return this.appService.getHello(); return this.appService.getHello();
} }
@Get('/sub')
subscribe(@Req() req: Request, @Res() res: Response) {
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
};
res.writeHead(200, headers);
this.sseService.createClient(req, res);
}
} }

19
packages/bridge-server/src/race-results/race-results.controller.ts

@ -1,17 +1,14 @@
import { Controller, Get, Header, Inject, Param, Req, Res, StreamableFile, forwardRef } from '@nestjs/common'; import { Controller, Get, Param, Res, StreamableFile, forwardRef } from '@nestjs/common';
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import { join } from 'path'; import { join } from 'path';
import type { Request, Response } from 'express'; import type { Response } from 'express';
import { RaceResultsService } from './race-results.service'; import { RaceResultsService } from './race-results.service';
import { SseService } from 'src/sse/sse.service';
@Controller('race-results') @Controller('race-results')
export class RaceResultsController { export class RaceResultsController {
constructor( constructor(
private raceResultsService: RaceResultsService, private raceResultsService: RaceResultsService,
@Inject(forwardRef(() => SseService))
private sseService: SseService,
) )
{} {}
@ -35,16 +32,4 @@ export class RaceResultsController {
const file = createReadStream(join(process.cwd(), result.replayPath)); const file = createReadStream(join(process.cwd(), result.replayPath));
file.pipe(res); file.pipe(res);
} }
@Get('/sub')
subscribe(@Req() req: Request, @Res() res: Response) {
const headers = {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache'
};
res.writeHead(200, headers);
this.sseService.createClient('race-results.updated', req, res);
}
} }

4
packages/bridge-server/src/race-results/race-results.module.ts

@ -3,12 +3,10 @@ import { SequelizeModule } from '@nestjs/sequelize';
import { RaceResult } from './race-result.model'; import { RaceResult } from './race-result.model';
import { RaceResultsService } from './race-results.service'; import { RaceResultsService } from './race-results.service';
import { RaceResultsController } from './race-results.controller'; import { RaceResultsController } from './race-results.controller';
import { SseModule } from 'src/sse/sse.module';
@Module({ @Module({
imports: [ imports: [
SequelizeModule.forFeature([RaceResult]), SequelizeModule.forFeature([RaceResult])
forwardRef(() => SseModule)
], ],
providers: [RaceResultsService], providers: [RaceResultsService],
exports: [SequelizeModule, RaceResultsService], exports: [SequelizeModule, RaceResultsService],

13
packages/bridge-server/src/season-standings/season-standings.service.ts

@ -6,6 +6,8 @@ import { RaceResultsService } from 'src/race-results/race-results.service';
import { RacesService } from 'src/races/races.service'; import { RacesService } from 'src/races/races.service';
import { Racer } from 'src/racers/racer.model'; import { Racer } from 'src/racers/racer.model';
import { RaceResult } from 'src/race-results/race-result.model'; import { RaceResult } from 'src/race-results/race-result.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { SeasonStandingsUpdated } from './seasons-standings-updated.event';
@Injectable() @Injectable()
export class SeasonStandingsService { export class SeasonStandingsService {
@ -14,7 +16,8 @@ export class SeasonStandingsService {
@InjectModel(SeasonStanding) private seasonStandingModel: typeof SeasonStanding, @InjectModel(SeasonStanding) private seasonStandingModel: typeof SeasonStanding,
private sequelize: Sequelize, private sequelize: Sequelize,
private raceResultsService: RaceResultsService, private raceResultsService: RaceResultsService,
private racesService: RacesService private racesService: RacesService,
private eventEmitter: EventEmitter2
) )
{} {}
@ -85,5 +88,13 @@ export class SeasonStandingsService {
console.log(entry); console.log(entry);
this.seasonStandingModel.upsert({seasonId: seasonId, racerId: entry[0], points: entry[1].points}); this.seasonStandingModel.upsert({seasonId: seasonId, racerId: entry[0], points: entry[1].points});
} }
let event = new SeasonStandingsUpdated();
event.seasonId = seasonId;
this.eventEmitter.emit(
'season-standings.updated',
event,
);
} }
} }

3
packages/bridge-server/src/season-standings/seasons-standings-updated.event.ts

@ -0,0 +1,3 @@
export class SeasonStandingsUpdated {
seasonId: number;
}

2
packages/bridge-server/src/seasons/seasons.service.ts

@ -15,7 +15,7 @@ export class SeasonsService {
{} {}
async findAll() { async findAll() {
return this.seasonModel.findAll(); return this.seasonModel.findAll({include:[Race]});
} }
async findOne(id: number) { async findOne(id: number) {

4
packages/bridge-server/src/sse/sse.module.ts

@ -1,10 +1,12 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module, forwardRef } from '@nestjs/common';
import { SseService } from './sse.service'; import { SseService } from './sse.service';
import { RaceResultsModule } from 'src/race-results/race-results.module'; import { RaceResultsModule } from 'src/race-results/race-results.module';
import { SeasonStandingsModule } from 'src/season-standings/season-standings.module';
@Module({ @Module({
imports:[ imports:[
forwardRef(() => RaceResultsModule) forwardRef(() => RaceResultsModule),
forwardRef(() => SeasonStandingsModule)
], ],
providers: [SseService], providers: [SseService],
exports: [SseService], exports: [SseService],

39
packages/bridge-server/src/sse/sse.service.ts

@ -3,6 +3,8 @@ import { OnEvent } from '@nestjs/event-emitter';
import { Response, Request } from 'express'; import { Response, Request } from 'express';
import { RaceResultCreated } from 'src/race-results/race-result-created.event'; import { RaceResultCreated } from 'src/race-results/race-result-created.event';
import { RaceResultsService } from 'src/race-results/race-results.service'; import { RaceResultsService } from 'src/race-results/race-results.service';
import { SeasonStandingsService } from 'src/season-standings/season-standings.service';
import { SeasonStandingsUpdated } from 'src/season-standings/seasons-standings-updated.event';
export class SSEClient { export class SSEClient {
id: number; id: number;
@ -11,53 +13,48 @@ export class SSEClient {
@Injectable() @Injectable()
export class SseService { export class SseService {
clients: Map<string, SSEClient[]>; clients: SSEClient[];
constructor( constructor(
private raceResultsService: RaceResultsService private raceResultsService: RaceResultsService,
private seasonStandingsService: SeasonStandingsService
) )
{ {
this.clients = new Map<string, SSEClient[]>; this.clients = [];
} }
createClient(event: string, req: Request, res: Response) { createClient(req: Request, res: Response) {
if(this.clients.has(event) == false) {
this.clients.set(event, []);
}
let newClient = new SSEClient(); let newClient = new SSEClient();
newClient.connection = res; newClient.connection = res;
newClient.id = Date.now(); // replace this. newClient.id = Date.now(); // replace this.
this.clients.get(event).push(newClient); this.clients.push(newClient);
const clientId = newClient.id; const clientId = newClient.id;
req.on('close', () => { req.on('close', () => {
console.log(`${clientId} Connection closed`); console.log(`${clientId} Connection closed`);
for(let events of this.clients) this.clients = this.clients.filter(client => client.id !== clientId);
{
events[1] = events[1].filter(client => client.id !== clientId);
}
}) })
} }
@OnEvent('race-result.created') @OnEvent('race-result.created')
async handleRaceResultcreated(payload: RaceResultCreated) { async handleRaceResultCreated(payload: RaceResultCreated) {
let updateResults = await this.raceResultsService.getFastestTimesForRace(payload.raceId); let updateResults = await this.raceResultsService.getFastestTimesForRace(payload.raceId);
this.raiseEvent('race-results.updated', {raceId: payload.raceId, newResults: updateResults}); this.raiseEvent({eventId: 'race-results.updated', raceId: payload.raceId, newResults: updateResults});
} }
raiseEvent(event:string, data:any) { @OnEvent('season-standings.updated')
if(this.clients.has(event) == false) { async handleSeasonStandingsUpdated(payload: SeasonStandingsUpdated) {
console.log("no clients for " + event) let updateResults = await this.seasonStandingsService.findManyForSeason(payload.seasonId);
return; this.raiseEvent({eventId: 'season-standings.updated', seasonId: payload.seasonId, newStandings: updateResults});
} }
raiseEvent(data:any) {
let data_packet = `data: ${JSON.stringify(data)}\n\n`; let data_packet = `data: ${JSON.stringify(data)}\n\n`;
console.log(data_packet); console.log(data_packet);
let clients = this.clients.get(event); for( let client of this.clients) {
for( let client of clients) {
client.connection.write(data_packet); client.connection.write(data_packet);
} }
} }

3
packages/bridge-ui/package.json

@ -21,6 +21,7 @@
"@angular/platform-browser-dynamic": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0", "@angular/router": "^17.0.0",
"bridge-shared": "^1.0.0", "bridge-shared": "^1.0.0",
"eventemitter3": "^5.0.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@ -31,7 +32,7 @@
"@angular/cli": "^17.0.1", "@angular/cli": "^17.0.1",
"@angular/compiler-cli": "^17.0.0", "@angular/compiler-cli": "^17.0.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"@types/node": "^20.9.2", "@types/node": "^20.10.0",
"jasmine-core": "~5.1.0", "jasmine-core": "~5.1.0",
"karma": "~6.4.0", "karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",

32
packages/bridge-ui/src/app/components/race-details/race-details.component.ts

@ -11,6 +11,7 @@ import { RaceResultService } from '../../services/race-result.service';
import { Race } from '../../models/race.model'; import { Race } from '../../models/race.model';
import { Racer } from '../../models/racer.model'; import { Racer } from '../../models/racer.model';
import { RaceEntry } from '../../models/raceEntry.model'; import { RaceEntry } from '../../models/raceEntry.model';
import { ServerSideEventsService } from '../../services/server-side-events.service';
@Component({ @Component({
selector: 'app-race-details', selector: 'app-race-details',
@ -64,12 +65,14 @@ export class RaceDetailsComponent implements AfterViewInit {
@ViewChild('minutes') minutes?: ElementRef; @ViewChild('minutes') minutes?: ElementRef;
@ViewChild('seconds') seconds?: ElementRef; @ViewChild('seconds') seconds?: ElementRef;
events?: EventSource; updatedResultsListener: any;
constructor( constructor(
private raceResultService: RaceResultService, private raceResultService: RaceResultService,
private racesService: RacesService, private racesService: RacesService,
) {} private sseService: ServerSideEventsService
)
{}
ngOnInit() { ngOnInit() {
if( this.race == undefined) { if( this.race == undefined) {
@ -93,24 +96,11 @@ export class RaceDetailsComponent implements AfterViewInit {
this.updateRaceResults(data); this.updateRaceResults(data);
}) })
this.events = new EventSource("http://localhost:3000/race-results/sub"); this.updatedResultsListener = this.sseService.registerEventHandler('race-results.updated', this.onUpdatedRaceResults, this);
this.events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
console.log(parsedData);
if(this.race != undefined && parsedData.raceId != this.race.id){
return;
}
this.updateRaceResults(parsedData.newResults);
}
} }
ngOnDestroy() { ngOnDestroy() {
if( this.race == undefined) { this.sseService.unregisterEventHandler('race-results.updated', this.updatedResultsListener);
return;
}
this.events?.close();
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -178,6 +168,14 @@ export class RaceDetailsComponent implements AfterViewInit {
}); });
} }
onUpdatedRaceResults(data: any) {
if(this.race != undefined && data.raceId != this.race.id){
return;
}
this.updateRaceResults(data.newResults);
}
updateRaceResults( data: any ) { updateRaceResults( data: any ) {
this.raceResults = data; this.raceResults = data;

4
packages/bridge-ui/src/app/components/season-card/season-card.component.ts

@ -21,12 +21,12 @@ export class SeasonCardComponent {
@Input() season?: Season; @Input() season?: Season;
getStartingDate() { getStartingDate() {
if(this.season != undefined){ if(this.season != undefined) {
let date = new Date(0); let date = new Date(0);
date.setUTCSeconds(Number(this.season.startingDate)); date.setUTCSeconds(Number(this.season.startingDate));
return date.toUTCString(); return date.toUTCString();
} }
return ""; return "";
} }

115
packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts

@ -11,6 +11,7 @@ import { RaceEntry } from '../../models/raceEntry.model';
import { Season } from '../../models/season.model'; import { Season } from '../../models/season.model';
import { RacesService } from '../../services/races.service'; import { RacesService } from '../../services/races.service';
import { SeasonStanding } from '../../models/season-standing.model'; import { SeasonStanding } from '../../models/season-standing.model';
import { ServerSideEventsService } from '../../services/server-side-events.service';
@Component({ @Component({
selector: 'app-season-standings', selector: 'app-season-standings',
@ -36,10 +37,13 @@ export class SeasonStandingsComponent {
displayedColumns: string[] = ['position', 'name', 'points']; displayedColumns: string[] = ['position', 'name', 'points'];
sortedStandings!: SeasonStanding[]; sortedStandings!: SeasonStanding[];
updatedStandingsListener: any;
constructor( constructor(
private racersService: RacersService, private racersService: RacersService,
private raceResultService: RaceResultService, private raceResultService: RaceResultService,
private racesService: RacesService, private racesService: RacesService,
private sseService: ServerSideEventsService,
) {} ) {}
ngOnInit() { ngOnInit() {
@ -57,108 +61,39 @@ export class SeasonStandingsComponent {
console.log(this.sortedStandings); console.log(this.sortedStandings);
console.log("Season Standings - end") console.log("Season Standings - end")
return; this.updatedStandingsListener = this.sseService.registerEventHandler('season-standings.updated', this.onUpdatedStandings, this);
/*
for (let race_id of this.season.races) {
this.racesService.getRace(race_id).subscribe( race => {
if( race != undefined ){
this.races.push(race);
this.seasonRaceResults.set(race.id, []);
for( let racer_id of race.racers ) {
if( this.seasonRacers.has(racer_id) == false ) {
this.racersService.getRacer(racer_id).subscribe( data => {
if(data != undefined) {
this.seasonRacers.set(racer_id, data);
}
});
}
let thisRaceResults = this.seasonRaceResults.get(race.id);
this.raceResultService.getRaceResultsForRacerInRace(racer_id, race.id).subscribe( data => {
if(data != undefined) {
let bestResult = this.getBestTime(data);
if(thisRaceResults != undefined) {
thisRaceResults.push(bestResult);
}
this.calculateSeasonPoints();
}
});
}
}
});
}*/
} }
getRacerName(racer: any): string { ngOnDestroy() {
if( racer != undefined) { this.sseService.unregisterEventHandler('season-standings.updated', this.updatedStandingsListener);
if( racer.name == undefined || racer.name == "" ) {
return racer.gameHandle;
}
return racer.name + " (" + racer.gameHandle + ")"
}
return ""
}
getBestTime(results: RaceEntry[]): RaceEntry {
let bestTime: number = 0xFFFFFFFF;
let bestResult = new RaceEntry();
for(let result of results){
if(result.time < bestTime) {
bestTime = result.time
bestResult = result;
}
}
return bestResult
} }
/* onUpdatedStandings(data: any) {
calculateSeasonPoints() { if(this.season != undefined && data.seasonId != this.season.id){
if(this.races == undefined) {
return; return;
} }
let maxRacePoints = this.seasonRacers.size; let sorted = []
for(let result of data.newStandings){
sorted.push(result)
}
sorted.sort((a, b) => {
return a.points - b.points
})
this.seasonPoints = new Map<string, SeasonStanding>(); this.sortedStandings = [...sorted];
}
for( let race of this.races) { getRacerName(racer: any): string {
let availablePoints = maxRacePoints; if( racer != undefined) {
let thisRaceResults = this.seasonRaceResults.get(race.id); if( racer.name == undefined || racer.name == "" ) {
if(thisRaceResults == undefined) { return racer.gameHandle;
continue
} }
thisRaceResults.sort((a,b) =>{ return racer.name + " (" + racer.gameHandle + ")"
return a.time - b.time;
})
for (let result of thisRaceResults) {
if (this.seasonPoints.has(result.racer_id) == false) {
this.seasonPoints.set(result.racer_id, new SeasonStanding(result.racer_id));
}
let currentPoints = this.seasonPoints.get(result.racer_id);
if( currentPoints == undefined ) {
continue;
}
currentPoints.points+=availablePoints;
availablePoints -=1;
}
} }
this.sortedStandings = Array.from(this.seasonPoints.values()).sort((a, b) => { return ""
return b.points - a.points;
})
this.sortedStandings = [...this.sortedStandings]
} }
*/
} }

16
packages/bridge-ui/src/app/services/server-side-events.service.spec.ts

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ServerSideEventsService } from './server-side-events.service';
describe('ServerSideEventsService', () => {
let service: ServerSideEventsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ServerSideEventsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

45
packages/bridge-ui/src/app/services/server-side-events.service.ts

@ -0,0 +1,45 @@
import { Injectable } from '@angular/core';
import { ServerEndpointService } from './server-endpoint.service';
//var EventEmitter2 = require('eventemitter2');
import { EventEmitter } from 'eventemitter3';
@Injectable({
providedIn: 'root'
})
export class ServerSideEventsService {
events: EventSource;
emitter: EventEmitter;
constructor(
private serverEndpointService: ServerEndpointService
)
{
this.emitter = new EventEmitter();
console.log(this.emitter)
this.events = new EventSource(this.serverEndpointService.getCurrentEndpoint()+"sub");
this.events.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
console.log(parsedData);
this.emitter.emit(parsedData.eventId, parsedData);
}
}
onServerEventRecieved(event: any) {
const parsedData = JSON.parse(event.data);
console.log(parsedData);
this.emitter.emit(parsedData.eventId, parsedData, {objectify: true});
}
registerEventHandler(eventId: string, func: any, context: any) {
return this.emitter.addListener(eventId, func, context);
}
unregisterEventHandler(eventId: string, listener: any) {
this.emitter.removeListener(eventId, listener);
}
}

3
packages/bridge-ui/tsconfig.json

@ -21,6 +21,9 @@
"lib": [ "lib": [
"ES2022", "ES2022",
"dom" "dom"
],
"types": [
"node"
] ]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {

Loading…
Cancel
Save