Skip to content

Commit

Permalink
feat: SSE reconnect after disconnect
Browse files Browse the repository at this point in the history
  • Loading branch information
cbartel-ci committed Dec 3, 2021
1 parent eae55c7 commit 463a62e
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 29 deletions.
4 changes: 4 additions & 0 deletions libs/model/src/event.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export interface Event {
}

export type EventType<T extends Event> = new (...params: never[]) => T;

export class KeepAliveEvent implements Event {
id: 'KEEPALIVE';
}
8 changes: 6 additions & 2 deletions server/src/event/event.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Controller, Sse } from '@nestjs/common';
import { Observable, Subject } from 'rxjs';
import { interval, Observable, Subject } from 'rxjs';
import { RequiredPermissions } from '../login/login.decorator';
import { Permission } from '@nw-company-tool/model';
import { KeepAliveEvent, Permission } from '@nw-company-tool/model';
import { OnEvent } from '@nestjs/event-emitter';

@Controller('/api/event')
@RequiredPermissions(Permission.ENABLED)
export class EventController {
private event$ = new Subject<string>();

constructor() {
interval(15000).subscribe(() => this.event$.next(JSON.stringify(new KeepAliveEvent())));
}

@Sse('/')
sse(): Observable<string> {
return new Observable((observer) => {
Expand Down
4 changes: 1 addition & 3 deletions webapp/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import { Router, Routes } from '@angular/router';
import { PluginDefinition } from './services/plugin/plugin.model';
import { loadRemoteModule } from '@angular-architects/module-federation';
import { routes } from './app-routing.module';
import { EventService } from './services/event/event.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
constructor(private router: Router, private pluginService: PluginService, private eventService: EventService) {}
constructor(private router: Router, private pluginService: PluginService) {}

async ngOnInit(): Promise<void> {
const plugins = await this.pluginService.getPlugins().toPromise();
const routes = this.buildRoutes(plugins);
this.router.resetConfig(routes);
this.eventService.init();
}

buildRoutes(plugins: PluginDefinition[]): Routes {
Expand Down
7 changes: 6 additions & 1 deletion webapp/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ExpeditionModule } from './services/expedition/expedition.module';
import { SnackbarModule } from './services/snackbar/snackbar.module';
import { HeaderModule } from './components/header/header.module';
import { FooterModule } from './components/footer/footer.module';
import { EventService } from './services/event/event.service';

const cookieConfig: NgcCookieConsentConfig = {
cookie: {
Expand Down Expand Up @@ -91,5 +92,9 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl
exports: []
})
export class AppModule {
constructor(private ccService: NgcCookieConsentService, private configService: ConfigService) {}
constructor(
private ccService: NgcCookieConsentService,
private configService: ConfigService,
private eventService: EventService
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit {
this.expeditionService.getExpeditions().subscribe((data) => {
this.dataSource.data = data;
});
this.expeditionService.refreshExpeditions();
}

ngAfterViewInit(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { CalendarOptions, EventInput, FullCalendarComponent } from '@fullcalendar/angular';
import * as moment from 'moment';
import { ExpeditionService } from '../../../../services/expedition/expedition.service';
Expand All @@ -14,7 +14,7 @@ import { Observable, Subscription } from 'rxjs';
templateUrl: './home-calendar.component.html',
styleUrls: ['./home-calendar.component.css']
})
export class HomeCalendarComponent implements AfterViewInit, OnDestroy {
export class HomeCalendarComponent implements OnInit, AfterViewInit, OnDestroy {
options: CalendarOptions = {
initialDate: moment().format('yyyy-MM-DD'),
headerToolbar: {
Expand Down Expand Up @@ -51,6 +51,20 @@ export class HomeCalendarComponent implements AfterViewInit, OnDestroy {
private translate: TranslateService
) {}

ngOnInit(): void {
this.expeditionService.refreshExpeditions();
}

ngAfterViewInit(): void {
this.updateSubscription = this.expeditionService
.getExpeditions()
.subscribe(() => this.calendarComponent.getApi().refetchEvents());
}

ngOnDestroy(): void {
this.updateSubscription?.unsubscribe();
}

private handleEventClick(info: EventClickArg): void {
if (info.event.extendedProps.type === CalendarEventType.EXPEDITION) {
this.router.navigate(['expedition']);
Expand Down Expand Up @@ -94,14 +108,4 @@ export class HomeCalendarComponent implements AfterViewInit, OnDestroy {
}
};
}

ngAfterViewInit(): void {
this.updateSubscription = this.expeditionService
.getExpeditions()
.subscribe(() => this.calendarComponent.getApi().refetchEvents());
}

ngOnDestroy(): void {
this.updateSubscription?.unsubscribe();
}
}
66 changes: 56 additions & 10 deletions webapp/src/app/services/event/event.service.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,81 @@
import { Injectable, NgZone } from '@angular/core';
import { UserService } from '../user/user.service';
import { Event, EventType } from '@nw-company-tool/model';
import { Observable, Subject } from 'rxjs';
import { Event, EventType, KeepAliveEvent } from '@nw-company-tool/model';
import { interval, Observable, Subject, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import * as moment from 'moment';

@Injectable({
providedIn: 'root'
})
export class EventService {
private eventSource: EventSource;
private events$ = new Subject<Event>();
private lastKeepAlive: number;
private keepAliveSubscription: Subscription;
private reconnectSubscription: Subscription;

constructor(private zone: NgZone, private userService: UserService) {}

init(): void {
constructor(private zone: NgZone, private userService: UserService) {
this.userService.isLoggedIn$().subscribe((loggedIn) => {
if (loggedIn) {
this.eventSource = new EventSource('/api/event', { withCredentials: true });
this.eventSource.onmessage = (event) => {
this.zone.run(() => this.onEvent(JSON.parse(event.data)));
};
this.connect();
} else {
this.eventSource?.close();
}
});
this.subscribe(KeepAliveEvent).subscribe(() => this.onKeepAliveEvent());
}

private connect() {
this.eventSource = new EventSource('/api/event', { withCredentials: true });
this.eventSource.onmessage = (event) => {
this.zone.run(() => this.onEvent(JSON.parse(event.data)));
};
this.eventSource.onerror = () => {
this.disconnect();
this.reconnect();
};
this.eventSource.onopen = () => {
this.lastKeepAlive = moment.now();
console.info('connected to server');
this.reconnectSubscription?.unsubscribe();
this.reconnectSubscription = undefined;
this.keepAliveSubscription = interval(1000).subscribe(() => this.checkKeepAlive());
};
}

private disconnect() {
this.keepAliveSubscription?.unsubscribe();
this.eventSource?.close();
}

private reconnect() {
if (this.reconnectSubscription) {
// already trying to reconnect...
return;
}
this.reconnectSubscription = interval(2000).subscribe(() => {
console.log('reconnecting...');
this.connect();
});
}

private onEvent(event: Event) {
private onEvent(event: Event): void {
this.events$.next(event);
}

private onKeepAliveEvent(): void {
this.lastKeepAlive = moment.now();
}

private checkKeepAlive() {
if (moment.now() - this.lastKeepAlive > 20000) {
console.error('connection seems dead, trying to reconnect...');
this.disconnect();
this.reconnect();
}
}

subscribe<T extends Event>(eventType: EventType<T>): Observable<T> {
const eventId = new eventType().id;
return this.events$.pipe(
Expand Down
1 change: 0 additions & 1 deletion webapp/src/app/services/expedition/expedition.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export class ExpeditionService {
this.eventService.subscribe(ExpeditionDeleteEvent).subscribe((event) => this.onExpeditionDelete(event));
this.eventService.subscribe(ExpeditionJoinEvent).subscribe((event) => this.onExpeditionJoin(event));
this.eventService.subscribe(ExpeditionLeaveEvent).subscribe((event) => this.onExpeditionLeave(event));
this.refreshExpeditions();
}

public getExpeditions(): Observable<Expedition[]> {
Expand Down

0 comments on commit 463a62e

Please sign in to comment.