Browse Source

Add in SSE for race results

new_auth
Quildra 2 years ago
parent
commit
0354e8d55e
  1. 19
      package-lock.json
  2. 1
      packages/bridge-server/package.json
  3. 22
      packages/bridge-server/src/app.module.ts
  4. 4
      packages/bridge-server/src/race-results/race-result-created.event.ts
  5. 21
      packages/bridge-server/src/race-results/race-results.controller.ts
  6. 8
      packages/bridge-server/src/race-results/race-results.module.ts
  7. 16
      packages/bridge-server/src/race-results/race-results.service.ts
  8. 12
      packages/bridge-server/src/sse/sse.module.ts
  9. 18
      packages/bridge-server/src/sse/sse.service.spec.ts
  10. 64
      packages/bridge-server/src/sse/sse.service.ts
  11. 4
      packages/bridge-server/src/upload/upload.service.ts
  12. 46
      packages/bridge-ui/src/app/components/race-details/race-details.component.ts
  13. 4
      packages/bridge-ui/src/app/components/upload-replay-dialog/upload-replay-dialog.component.ts
  14. 8
      packages/bridge-ui/src/app/services/races.service.ts

19
package-lock.json

@ -5863,6 +5863,19 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz",
"integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA=="
},
"node_modules/@nestjs/event-emitter": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.3.tgz",
"integrity": "sha512-Pt7KAERrgK0OjvarSI3wfVhwZ8X1iLq1lXuodyRe+Zx3aLLP7fraFUHirASbFkB6KIQ1Zj+gZ1g8a9eu4GfFhw==",
"dependencies": {
"eventemitter2": "6.4.9"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0",
"reflect-metadata": "^0.1.12"
}
},
"node_modules/@nestjs/jwt": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.2.0.tgz",
@ -11555,6 +11568,11 @@
"integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==",
"dev": true
},
"node_modules/eventemitter2": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg=="
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@ -23430,6 +23448,7 @@
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/sequelize": "^10.0.0",

1
packages/bridge-server/package.json

@ -22,6 +22,7 @@
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/sequelize": "^10.0.0",

22
packages/bridge-server/src/app.module.ts

@ -3,6 +3,7 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MulterModule } from '@nestjs/platform-express'
import { EventEmitterModule } from '@nestjs/event-emitter';
import { SequelizeModule } from '@nestjs/sequelize';
import { AuthModule } from './auth/auth.module';
@ -22,6 +23,8 @@ import { RacesModule } from './races/races.module';
import { RacersModule } from './racers/racers.module';
import { SeasonStandingsModule } from './season-standings/season-standings.module';
import { UploadModule } from './upload/upload.module';
import { SseService } from './sse/sse.service';
import { SseModule } from './sse/sse.module';
@Module({
imports: [
@ -31,6 +34,22 @@ import { UploadModule } from './upload/upload.module';
autoLoadModels: true,
synchronize: true,
}),
EventEmitterModule.forRoot({
// set this to `true` to use wildcards
wildcard: false,
// the delimiter used to segment namespaces
delimiter: '.',
// set this to `true` if you want to emit the newListener event
newListener: false,
// set this to `true` if you want to emit the removeListener event
removeListener: false,
// the maximum amount of listeners that can be assigned to an event
maxListeners: 10,
// show event name in memory leak message when more than maximum amount of listeners is assigned
verboseMemoryLeak: false,
// disable throwing uncaughtException if an error event is emitted and it has no listeners
ignoreErrors: false,
}),
AuthModule,
UsersModule,
MulterModule.register({
@ -42,9 +61,10 @@ import { UploadModule } from './upload/upload.module';
RacersModule,
SeasonStandingsModule,
UploadModule,
SseModule,
],
controllers: [AppController, SeasonsController, UploadController],
providers: [AppService, UsersService, RacersService, RacesService, SeasonStandingsService, RaceResultsService],
providers: [AppService, UsersService, RacersService, RacesService, SeasonStandingsService, RaceResultsService, SseService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {

4
packages/bridge-server/src/race-results/race-result-created.event.ts

@ -0,0 +1,4 @@
export class RaceResultCreated {
raceId: number;
racerId: number;
}

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

@ -1,14 +1,17 @@
import { Controller, Get, Header, Param, Res, StreamableFile } from '@nestjs/common';
import { Controller, Get, Header, Inject, Param, Req, Res, StreamableFile, forwardRef } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
import type { Response } from 'express';
import type { Request, 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
private raceResultsService: RaceResultsService,
@Inject(forwardRef(() => SseService))
private sseService: SseService,
)
{}
@ -32,4 +35,16 @@ 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);
}
}

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

@ -1,11 +1,15 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
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])],
imports: [
SequelizeModule.forFeature([RaceResult]),
forwardRef(() => SseModule)
],
providers: [RaceResultsService],
exports: [SequelizeModule, RaceResultsService],
controllers: [RaceResultsController]

16
packages/bridge-server/src/race-results/race-results.service.ts

@ -3,13 +3,16 @@ import { InjectModel } from '@nestjs/sequelize';
import { RaceResult } from './race-result.model';
import { Sequelize } from 'sequelize-typescript';
import { Racer } from 'src/racers/racer.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { RaceResultCreated } from './race-result-created.event';
@Injectable()
export class RaceResultsService {
constructor(
@InjectModel(RaceResult) private raceResultModel: typeof RaceResult,
private sequelize: Sequelize
private sequelize: Sequelize,
private eventEmitter: EventEmitter2
)
{}
@ -33,7 +36,7 @@ export class RaceResultsService {
try {
await this.sequelize.transaction( async t => {
const transactionHost = { transaction: t };
await this.raceResultModel.create({
let newResult = await this.raceResultModel.create({
raceId: raceId,
racerId: racerId,
replayPath: replayPath,
@ -41,6 +44,15 @@ export class RaceResultsService {
},
transactionHost
);
let event = new RaceResultCreated();
event.raceId = raceId;
event.racerId = racerId;
this.eventEmitter.emit(
'race-result.created',
event,
);
});
} catch (error) {

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

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

18
packages/bridge-server/src/sse/sse.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SseService } from './sse.service';
describe('SseService', () => {
let service: SseService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SseService],
}).compile();
service = module.get<SseService>(SseService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

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

@ -0,0 +1,64 @@
import { Injectable } from '@nestjs/common';
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';
export class SSEClient {
id: number;
connection: Response;
}
@Injectable()
export class SseService {
clients: Map<string, SSEClient[]>;
constructor(
private raceResultsService: RaceResultsService
)
{
this.clients = new Map<string, SSEClient[]>;
}
createClient(event: string, req: Request, res: Response) {
if(this.clients.has(event) == false) {
this.clients.set(event, []);
}
let newClient = new SSEClient();
newClient.connection = res;
newClient.id = Date.now(); // replace this.
this.clients.get(event).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);
}
})
}
@OnEvent('race-result.created')
async handleRaceResultcreated(payload: RaceResultCreated) {
let updateResults = await this.raceResultsService.getFastestTimesForRace(payload.raceId);
this.raiseEvent('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;
}
let data_packet = `data: ${JSON.stringify(data)}\n\n`;
console.log(data_packet);
let clients = this.clients.get(event);
for( let client of clients) {
client.connection.write(data_packet);
}
}
}

4
packages/bridge-server/src/upload/upload.service.ts

@ -44,6 +44,10 @@ export class UploadService {
let race = await this.racesService.findOneBySeasonAndMapUID(body.seasonId, replay.mapUID)
console.log(race);
if(race == undefined){
console.log("No race for " + replay.mapUID);
}
if(race.endDate.getTime() < Date.now()) {
console.log("Too Late!")
return;

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

@ -64,6 +64,8 @@ export class RaceDetailsComponent implements AfterViewInit {
@ViewChild('minutes') minutes?: ElementRef;
@ViewChild('seconds') seconds?: ElementRef;
events?: EventSource;
constructor(
private raceResultService: RaceResultService,
private racesService: RacesService,
@ -88,19 +90,27 @@ export class RaceDetailsComponent implements AfterViewInit {
this.racesService.getRaceResults(this.race.id).subscribe(data => {
console.log(data)
this.raceResults = data;
this.updateRaceResults(data);
})
for(let result of this.raceResults){
this.sortedResults.push(result)
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.sortedResults.sort((a, b) => {
if( a.time == b.time ) { return 0 };
if( a.time < b.time ) { return -1};
return 1;
})
this.sortedResults = [...this.sortedResults];
})
this.updateRaceResults(parsedData.newResults);
}
}
ngOnDestroy() {
if( this.race == undefined) {
return;
}
this.events?.close();
}
ngAfterViewInit(): void {
@ -167,4 +177,20 @@ export class RaceDetailsComponent implements AfterViewInit {
link.remove();
});
}
updateRaceResults( data: any ) {
this.raceResults = data;
let sorted = []
for(let result of this.raceResults){
sorted.push(result)
}
sorted.sort((a, b) => {
if( a.time == b.time ) { return 0 };
if( a.time < b.time ) { return -1};
return 1;
})
this.sortedResults = [...sorted];
}
}

4
packages/bridge-ui/src/app/components/upload-replay-dialog/upload-replay-dialog.component.ts

@ -65,8 +65,6 @@ export class UploadReplayDialogComponent {
let local = this.seasonId;
formData.append("seasonId", local);
const upload$ = this.replaysService.uploadReplay(formData);
upload$.subscribe(data => {
window.location.reload();
});
upload$.subscribe();
}
}

8
packages/bridge-ui/src/app/services/races.service.ts

@ -35,4 +35,12 @@ export class RacesService {
create(mapUID: string, startDate: Date, endDate: Date, seasonId: number) {
return this.httpClient.post(this.serverEndpointService.getCurrentEndpoint()+"races", { mapUID: mapUID, startDate: startDate, endDate: endDate, seasonId: seasonId});
}
subscribeForUpdates(raceId: string) {
}
unsubscribeForUpdates(raceId: string) {
}
}

Loading…
Cancel
Save