Introduce Angular Chat Component with SSE integration for real-time updates.

This commit is contained in:
2026-07-19 00:22:54 +02:00
parent 04ca05cd14
commit aa0c61cf5e
8 changed files with 119 additions and 1 deletions
@@ -0,0 +1 @@
<p>chat works!</p>
@@ -0,0 +1,32 @@
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from 'rxjs';
import {ChatService} from '@pages/altitude/chat/service/chat.service';
@Component({
selector: 'app-chat',
imports: [],
templateUrl: './chat.component.html',
styleUrl: './chat.component.scss'
})
export class ChatComponent implements OnInit, OnDestroy {
private sub?: Subscription;
private readonly liveEvents: ChatService = inject(ChatService)
ngOnInit(): void {
this.liveEvents.connect();
this.sub = this.liveEvents.onEvent().subscribe(event => {
//TODO enums for event types
if (event.type === 'chat') {
console.log(event.data)
} else if (event.type === 'connect') {
console.log(event.data)
}
});
}
ngOnDestroy(): void {
this.sub?.unsubscribe();
this.liveEvents.disconnect(); // triggers onCompletion server-side, cleans up the emitter
}
}
@@ -0,0 +1,74 @@
import {inject, Injectable, OnDestroy} from '@angular/core';
import {Subject} from 'rxjs';
import {AuthService} from '@services/auth.service';
import {EventSourcePolyfill} from 'event-source-polyfill';
export interface ChatEvent {
type: string;
data: any;
}
interface SsePayloadEvent {
data: string;
}
@Injectable({providedIn: 'root'})
export class ChatService implements OnDestroy {
private eventSource?: EventSourcePolyfill;
private events$ = new Subject<ChatEvent>();
private readonly authService: AuthService = inject(AuthService)
connect() {
if (this.eventSource) {
return; // already connected
}
const jwt = this.authService.getJwt();
if (!jwt) {
//TODO [Stijn] [2026-07-18]: Error when no JWT available (log in?)
return;
}
const source = new EventSourcePolyfill('/api/chat/read/subscribe', {
headers: {
Authorization: `Bearer ${jwt}`
},
heartbeatTimeout: 60000
});
this.eventSource = source;
this.on(source, 'connected', (event) => {
console.log('SSE connected:', event.data);
});
this.on(source, 'chat-message', (event) => {
this.events$.next({type: 'chat-message', data: JSON.parse(event.data)});
});
source.onerror = (err) => {
console.error('SSE error, polyfill will auto-reconnect:', err);
};
}
// single, deliberate escape hatch from the broken DOM/polyfill type overlap —
// everything else in this file stays fully typed
private on(source: EventSourcePolyfill, eventName: string, handler: (event: SsePayloadEvent) => void): void {
(source as unknown as { addEventListener: (type: string, listener: (event: SsePayloadEvent) => void) => void })
.addEventListener(eventName, handler);
}
onEvent() {
return this.events$.asObservable();
}
disconnect(): void {
this.eventSource?.close();
this.eventSource = undefined;
}
ngOnDestroy(): void {
this.disconnect();
}
}