diff --git a/package-lock.json b/package-lock.json index e032211..952eb6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7492,9 +7492,9 @@ } }, "node_modules/@types/node": { - "version": "20.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz", - "integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==", + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -23522,6 +23522,7 @@ "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", "bridge-shared": "^1.0.0", + "eventemitter3": "^5.0.1", "jwt-decode": "^4.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -23532,7 +23533,7 @@ "@angular/cli": "^17.0.1", "@angular/compiler-cli": "^17.0.0", "@types/jasmine": "~5.1.0", - "@types/node": "^20.9.2", + "@types/node": "^20.10.0", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -23541,6 +23542,11 @@ "karma-jasmine-html-reporter": "~2.1.0", "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==" } } } diff --git a/packages/bridge-server/src/app.controller.ts b/packages/bridge-server/src/app.controller.ts index cce879e..42cb7cb 100644 --- a/packages/bridge-server/src/app.controller.ts +++ b/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 { Request, Response } from 'express'; +import { SseService } from './sse/sse.service'; @Controller() export class AppController { - constructor(private readonly appService: AppService) {} + constructor( + private readonly appService: AppService, + private sseService: SseService + ) + {} @Get() getHello(): string { 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); + } } diff --git a/packages/bridge-server/src/race-results/race-results.controller.ts b/packages/bridge-server/src/race-results/race-results.controller.ts index 3e772a5..b488a6b 100644 --- a/packages/bridge-server/src/race-results/race-results.controller.ts +++ b/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 { join } from 'path'; -import type { Request, Response } from 'express'; +import type { Response } from 'express'; import { RaceResultsService } from './race-results.service'; -import { SseService } from 'src/sse/sse.service'; @Controller('race-results') export class RaceResultsController { constructor( private raceResultsService: RaceResultsService, - @Inject(forwardRef(() => SseService)) - private sseService: SseService, ) {} @@ -35,16 +32,4 @@ export class RaceResultsController { const file = createReadStream(join(process.cwd(), result.replayPath)); 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); - } } diff --git a/packages/bridge-server/src/race-results/race-results.module.ts b/packages/bridge-server/src/race-results/race-results.module.ts index aeb8d59..47b75da 100644 --- a/packages/bridge-server/src/race-results/race-results.module.ts +++ b/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 { RaceResultsService } from './race-results.service'; import { RaceResultsController } from './race-results.controller'; -import { SseModule } from 'src/sse/sse.module'; @Module({ imports: [ - SequelizeModule.forFeature([RaceResult]), - forwardRef(() => SseModule) + SequelizeModule.forFeature([RaceResult]) ], providers: [RaceResultsService], exports: [SequelizeModule, RaceResultsService], diff --git a/packages/bridge-server/src/season-standings/season-standings.service.ts b/packages/bridge-server/src/season-standings/season-standings.service.ts index eb944a0..8c30099 100644 --- a/packages/bridge-server/src/season-standings/season-standings.service.ts +++ b/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 { Racer } from 'src/racers/racer.model'; import { RaceResult } from 'src/race-results/race-result.model'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { SeasonStandingsUpdated } from './seasons-standings-updated.event'; @Injectable() export class SeasonStandingsService { @@ -14,7 +16,8 @@ export class SeasonStandingsService { @InjectModel(SeasonStanding) private seasonStandingModel: typeof SeasonStanding, private sequelize: Sequelize, private raceResultsService: RaceResultsService, - private racesService: RacesService + private racesService: RacesService, + private eventEmitter: EventEmitter2 ) {} @@ -85,5 +88,13 @@ export class SeasonStandingsService { console.log(entry); 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, + ); } } diff --git a/packages/bridge-server/src/season-standings/seasons-standings-updated.event.ts b/packages/bridge-server/src/season-standings/seasons-standings-updated.event.ts new file mode 100644 index 0000000..5490b89 --- /dev/null +++ b/packages/bridge-server/src/season-standings/seasons-standings-updated.event.ts @@ -0,0 +1,3 @@ +export class SeasonStandingsUpdated { + seasonId: number; +} \ No newline at end of file diff --git a/packages/bridge-server/src/seasons/seasons.service.ts b/packages/bridge-server/src/seasons/seasons.service.ts index f094c9a..ac5175b 100644 --- a/packages/bridge-server/src/seasons/seasons.service.ts +++ b/packages/bridge-server/src/seasons/seasons.service.ts @@ -15,7 +15,7 @@ export class SeasonsService { {} async findAll() { - return this.seasonModel.findAll(); + return this.seasonModel.findAll({include:[Race]}); } async findOne(id: number) { diff --git a/packages/bridge-server/src/sse/sse.module.ts b/packages/bridge-server/src/sse/sse.module.ts index eda852c..51ace29 100644 --- a/packages/bridge-server/src/sse/sse.module.ts +++ b/packages/bridge-server/src/sse/sse.module.ts @@ -1,10 +1,12 @@ import { Module, forwardRef } from '@nestjs/common'; import { SseService } from './sse.service'; import { RaceResultsModule } from 'src/race-results/race-results.module'; +import { SeasonStandingsModule } from 'src/season-standings/season-standings.module'; @Module({ imports:[ - forwardRef(() => RaceResultsModule) + forwardRef(() => RaceResultsModule), + forwardRef(() => SeasonStandingsModule) ], providers: [SseService], exports: [SseService], diff --git a/packages/bridge-server/src/sse/sse.service.ts b/packages/bridge-server/src/sse/sse.service.ts index 2bce7a7..8ddbc9e 100644 --- a/packages/bridge-server/src/sse/sse.service.ts +++ b/packages/bridge-server/src/sse/sse.service.ts @@ -3,6 +3,8 @@ import { OnEvent } from '@nestjs/event-emitter'; import { Response, Request } from 'express'; import { RaceResultCreated } from 'src/race-results/race-result-created.event'; 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 { id: number; @@ -11,53 +13,48 @@ export class SSEClient { @Injectable() export class SseService { - clients: Map; + clients: SSEClient[]; constructor( - private raceResultsService: RaceResultsService + private raceResultsService: RaceResultsService, + private seasonStandingsService: SeasonStandingsService ) { - this.clients = new Map; + this.clients = []; } - createClient(event: string, req: Request, res: Response) { - if(this.clients.has(event) == false) { - this.clients.set(event, []); - } + createClient(req: Request, res: Response) { let newClient = new SSEClient(); newClient.connection = res; newClient.id = Date.now(); // replace this. - this.clients.get(event).push(newClient); + this.clients.push(newClient); const clientId = newClient.id; req.on('close', () => { console.log(`${clientId} Connection closed`); - for(let events of this.clients) - { - events[1] = events[1].filter(client => client.id !== clientId); - } + this.clients = this.clients.filter(client => client.id !== clientId); }) } @OnEvent('race-result.created') - async handleRaceResultcreated(payload: RaceResultCreated) { + async handleRaceResultCreated(payload: RaceResultCreated) { 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) { - if(this.clients.has(event) == false) { - console.log("no clients for " + event) - return; - } + @OnEvent('season-standings.updated') + async handleSeasonStandingsUpdated(payload: SeasonStandingsUpdated) { + let updateResults = await this.seasonStandingsService.findManyForSeason(payload.seasonId); + this.raiseEvent({eventId: 'season-standings.updated', seasonId: payload.seasonId, newStandings: updateResults}); + } + raiseEvent(data:any) { let data_packet = `data: ${JSON.stringify(data)}\n\n`; console.log(data_packet); - let clients = this.clients.get(event); - for( let client of clients) { + for( let client of this.clients) { client.connection.write(data_packet); } } diff --git a/packages/bridge-ui/package.json b/packages/bridge-ui/package.json index 57e8859..d40522e 100644 --- a/packages/bridge-ui/package.json +++ b/packages/bridge-ui/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", "bridge-shared": "^1.0.0", + "eventemitter3": "^5.0.1", "jwt-decode": "^4.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", @@ -31,7 +32,7 @@ "@angular/cli": "^17.0.1", "@angular/compiler-cli": "^17.0.0", "@types/jasmine": "~5.1.0", - "@types/node": "^20.9.2", + "@types/node": "^20.10.0", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/packages/bridge-ui/src/app/components/race-details/race-details.component.ts b/packages/bridge-ui/src/app/components/race-details/race-details.component.ts index c21b566..93a16c4 100644 --- a/packages/bridge-ui/src/app/components/race-details/race-details.component.ts +++ b/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 { Racer } from '../../models/racer.model'; import { RaceEntry } from '../../models/raceEntry.model'; +import { ServerSideEventsService } from '../../services/server-side-events.service'; @Component({ selector: 'app-race-details', @@ -64,12 +65,14 @@ export class RaceDetailsComponent implements AfterViewInit { @ViewChild('minutes') minutes?: ElementRef; @ViewChild('seconds') seconds?: ElementRef; - events?: EventSource; + updatedResultsListener: any; constructor( private raceResultService: RaceResultService, private racesService: RacesService, - ) {} + private sseService: ServerSideEventsService + ) + {} ngOnInit() { if( this.race == undefined) { @@ -93,24 +96,11 @@ export class RaceDetailsComponent implements AfterViewInit { this.updateRaceResults(data); }) - this.events = new EventSource("http://localhost:3000/race-results/sub"); - 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); - } + this.updatedResultsListener = this.sseService.registerEventHandler('race-results.updated', this.onUpdatedRaceResults, this); } ngOnDestroy() { - if( this.race == undefined) { - return; - } - this.events?.close(); + this.sseService.unregisterEventHandler('race-results.updated', this.updatedResultsListener); } 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 ) { this.raceResults = data; diff --git a/packages/bridge-ui/src/app/components/season-card/season-card.component.ts b/packages/bridge-ui/src/app/components/season-card/season-card.component.ts index 77bb5cd..2aa90f8 100644 --- a/packages/bridge-ui/src/app/components/season-card/season-card.component.ts +++ b/packages/bridge-ui/src/app/components/season-card/season-card.component.ts @@ -21,12 +21,12 @@ export class SeasonCardComponent { @Input() season?: Season; getStartingDate() { - if(this.season != undefined){ + if(this.season != undefined) { let date = new Date(0); date.setUTCSeconds(Number(this.season.startingDate)); return date.toUTCString(); } - + return ""; } diff --git a/packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts b/packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts index 814b256..8a1d691 100644 --- a/packages/bridge-ui/src/app/components/season-standings/season-standings.component.ts +++ b/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 { RacesService } from '../../services/races.service'; import { SeasonStanding } from '../../models/season-standing.model'; +import { ServerSideEventsService } from '../../services/server-side-events.service'; @Component({ selector: 'app-season-standings', @@ -36,10 +37,13 @@ export class SeasonStandingsComponent { displayedColumns: string[] = ['position', 'name', 'points']; sortedStandings!: SeasonStanding[]; + updatedStandingsListener: any; + constructor( private racersService: RacersService, private raceResultService: RaceResultService, private racesService: RacesService, + private sseService: ServerSideEventsService, ) {} ngOnInit() { @@ -57,108 +61,39 @@ export class SeasonStandingsComponent { console.log(this.sortedStandings); console.log("Season Standings - end") - return; - /* - 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(); - } - }); - } - } - }); - - }*/ + this.updatedStandingsListener = this.sseService.registerEventHandler('season-standings.updated', this.onUpdatedStandings, this); } - getRacerName(racer: any): string { - if( racer != undefined) { - 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 + ngOnDestroy() { + this.sseService.unregisterEventHandler('season-standings.updated', this.updatedStandingsListener); } - /* - calculateSeasonPoints() { - - if(this.races == undefined) { + onUpdatedStandings(data: any) { + if(this.season != undefined && data.seasonId != this.season.id){ 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(); + this.sortedStandings = [...sorted]; + } - for( let race of this.races) { - let availablePoints = maxRacePoints; - let thisRaceResults = this.seasonRaceResults.get(race.id); - if(thisRaceResults == undefined) { - continue + getRacerName(racer: any): string { + if( racer != undefined) { + if( racer.name == undefined || racer.name == "" ) { + return racer.gameHandle; } - thisRaceResults.sort((a,b) =>{ - 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; - } + return racer.name + " (" + racer.gameHandle + ")" } - this.sortedStandings = Array.from(this.seasonPoints.values()).sort((a, b) => { - return b.points - a.points; - }) - - this.sortedStandings = [...this.sortedStandings] + return "" } - */ } diff --git a/packages/bridge-ui/src/app/services/server-side-events.service.spec.ts b/packages/bridge-ui/src/app/services/server-side-events.service.spec.ts new file mode 100644 index 0000000..d402f09 --- /dev/null +++ b/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(); + }); +}); diff --git a/packages/bridge-ui/src/app/services/server-side-events.service.ts b/packages/bridge-ui/src/app/services/server-side-events.service.ts new file mode 100644 index 0000000..9eafc0f --- /dev/null +++ b/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); + } +} diff --git a/packages/bridge-ui/tsconfig.json b/packages/bridge-ui/tsconfig.json index 678336b..0e2503d 100644 --- a/packages/bridge-ui/tsconfig.json +++ b/packages/bridge-ui/tsconfig.json @@ -21,6 +21,9 @@ "lib": [ "ES2022", "dom" + ], + "types": [ + "node" ] }, "angularCompilerOptions": {