typescript - Angular'da SSE'yi uygulama
Test uygulamamda SSE uygulamaya çalışıyorum. Sunucu tarafı kuruldu ve sadece uç noktayı (api/v1/sse/document) kullanıyorum. Senaryo şu: Bir tarama yaptığımda, taramanın test istemcimde ve ana uygulamamda görünmesi gerekiyor. Sorun şu ki, sadece sayfayı yenilediğimde test istemcimde görünüyor. Biraz kod yazdım ama test istemcimdeki olayları otomatik olarak güncelleyebilmeyi hâlâ sağlayamıyorum.
bu benim yazdığım kod
sse.servis.ts
[document-list.component.ts]
public ngOnInit(): void {
this.getDocuments();
this.registerServerSentEvent();
}
public ngOnDestroy(): void {
this.closeServerSentEvent();
}
/**
* getDocuments: call the api service to get the list of documents.
*/
public getDocuments(): void {
this.aService.getDocuments().subscribe((documents: Documents[]) => {
this.documents = documents;
});
}
/**
* markDocumentAsProcessed: call the api service to mark the document as processed.
*/
public markDocumentAsProcessed(document: Documents): void {
this.aService.markDocumentProcessed(document.document.id).subscribe({
next: () => {
// Remove the processed document from the list
this.documents = this.documents.filter((doc) => doc.document.id !== document.document.id);
this.closeDialog();
},
error: (error) => {
console.log("markDocumentProcessed Error:", error);
// Handle the error here
},
});
}
/**
* showDetails: call the api service to get the document image and display it in a dialog.
*/
public showDetails(document: Documents): void {
this.aService.getDocumentImage(document.document.id).subscribe((image: Blob) => {
const url = window.URL.createObjectURL(image);
const safeUrl: SafeUrl = this.sanitizer.bypassSecurityTrustUrl(url);
this.selectedDocument = {...document, imageDataURL: safeUrl};
this.displayDialog = true;
});
}
/**
* closeDialog: close the dialog after pressing the process button.
*/
private closeDialog(): void {
this.displayDialog = false;
this.selectedDocument = null;
}
private registerServerSentEvent(): void {
const sseUrl = `${this.aService.config.url}api/v1/sse/document`;
this.sseSubscription = this.sseService.getServerSentEvent(sseUrl).subscribe((event: MessageEvent) => {
const documentEvent = JSON.parse(event.data);
const eventType = documentEvent.type;
const eventData = documentEvent.data;
switch (eventType) {
case "NewDocument": {
// Process new document event
break;
}
case "ViewDocument": {
// Process view document event
break;
}
case "WatchlistStatusUpdate": {
// Process watchlist status update event
break;
}
case "DocumentProcessed": {
// Process document processed event
const processedDocumentId = eventData.documentId;
this.updateProcessedDocument(processedDocumentId);
break;
}
}
});
}
private updateProcessedDocument(processedDocumentId: string): void {
// Find the processed document in the documents list
const processedDocumentIndex = this.documents.findIndex((doc) => doc.document.id === processedDocumentId);
if (processedDocumentIndex !== -1) {
// Remove the processed document from the list
this.documents.splice(processedDocumentIndex, 1);
// Update any other UI-related logic or perform additional actions as needed
}
}
private closeServerSentEvent(): void {
if (this.sseSubscription) {
this.sseSubscription.unsubscribe();
this.sseService.closeEventSource();
}
}
}
ve bu benim belgeyi vs. alan servisım
[a.service.ts]
public getDocuments(): Observable<Documents[]> {
let url = this.config.url;
if (!url.endsWith("/")) {
url += "/";
}
return this.http
.get<Documents[]>(`${url}api/v1/document/`, {
headers: {
authorization: this.config.authorization,
},
})
.pipe(
map((data) => {
return data;
}),
catchError((error) => {
throw error;
})
);
}
/**
* markDocumentProcessed: calls the api service to mark the document as processed.
*
*/
public markDocumentProcessed(documentId: string): Observable<Documents[]> {
let url = this.config.url;
if (!url.endsWith("/")) {
url += "/";
}
const requestBody = {
DocumentId: documentId,
};
return this.http
.post<Documents[]>(`${url}api/v1/document/processed`, requestBody, {
headers: {
authorization: this.config.authorization,
},
})
.pipe(
map((data) => {
return data;
}),
catchError((error) => {
throw error;
})
);
}
/**
* getDocumentImage: calls the api service to get the document image.
*
*/
public getDocumentImage(documentId: string): Observable<Blob> {
let url = this.config.url;
if (!url.endsWith("/")) {
url += "/";
}
return this.http.get(`${url}api/v1/document/${documentId}/image/Photo`, {
responseType: "blob",
headers: {
authorization: this.config.authorization,
},
});
}
Konsolda hiçbir hata almıyorum, endpoint ile 200 alıyorum ama test istemcim olayları otomatik olarak almıyor ve değişiklikleri görmek için yenilemem gerekiyor.
Cevap #1
Sorun şu ki SSE uç noktasını normal bir GET uç noktası olarak kullanıyorsunuz. Elbette güncellemeleri yalnızca siz talep ederseniz alırsınız, sizin durumunuzda sayfayı yeniden yükleyerek.
Bunu yapıyorsunuz (SSE servis ekran görüntünüzden alınmıştır):
public getServerSentEvent(url: string): Observable<MessageEvent> {
const token = this.aService.config.authorization;
// WARNING - SSE doesn't have an API for Headers, this won't work if used as proper SSE
const headers = new HttpHeaders({
Authorization: token,
});
return new Observable<MessageEvent>((observer) => {
// HERE - you are not creating the SSE Event Source, just getting from it
this.http.get(url, {headers, responseType: "text"}).subscribe({
//...
})
})
}
Yani SSE uç noktanızda HTTP GET çağrısı yapıyorsunuz ve sonra bunu bir SSE olayı gibi göstermeye çalışıyorsunuz. Gördüğünüz gibi bu işe yaramıyor.
Yeni bir EventSource açmalısınız ve bu aslında SSE'nin olması gerektiği gibi çalışacaktır:
// Creates SSE event source, handles SSE events
protected createSseEventSource(): void {
// Close event source if current instance of SSE service has some
if (this.eventSource) {
this.closeSseConnection();
this.eventSource = null;
}
// Open new channel, create new EventSource - provide your own URL
this.eventSource = new EventSource(this.yourSSEurl);
// Process default event
this.eventSource.onmessage = (event: MessageEvent) => {
this.zone.run(() => this.processSseEvent(event));
};
// Add your own EVENTS handling...
/*
enum SSE_EVENTS [
NewDocument: 'NewDocument',
//...
]
*/
Object.keys(SSE_EVENTS).forEach(key => {
this.eventSource.addEventListener(SSE_EVENTS[key], event => {
this.zone.run(() => this.processSseEvent(event));
});
});
// Process connection opened
this.eventSource.onopen = () => {
this.reconnectFrequencySec = 1;
};
// Process error
this.eventSource.onerror = (error: any) => {
this.reconnectOnError();
};
}
Eski cevabıma bak: Hata durumunda SSE yeniden bağlanmayı yönetme. Orada ihtiyacınız olan her şeyle neredeyse çalışan bir SSE servisi bulabilirsiniz.
Cevap #2
import { Injectable, NgZone } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';
import {
EventSourceController,
EventSourcePlus,
SseMessage,
} from 'event-source-plus';
import { CompanyService } from './company.service';
@Injectable({
providedIn: 'root',
})
export class SseService {
private eventSource: EventSourcePlus | null = null;
companyId!: string;
private isConnected = false;
private controller!: EventSourceController;
constructor(private zone: NgZone, companyService: CompanyService) {
companyService.selectedCompany$.subscribe((company) => {
this.companyId =
company?.companyProfileId || localStorage.getItem('companyId') || '';
});
}
/**
* Connects to an SSE endpoint and provides an observable for events.
* @param url - The URL for the SSE endpoint.
* @param headers - Optional headers for the request.
* @returns An Observable emitting MessageEvent data.
*/
connect(
url: string,
headers?: Record<string, string>
): Observable<MessageEvent> {
const subject = new ReplaySubject<MessageEvent>(1); // Buffer the last emitted event
// Close any existing connection
this.close();
// Initialize EventSourcePlus with custom headers if provided
this.eventSource = new EventSourcePlus(url, {
retryStrategy: 'always',
maxRetryCount: 4,
method: 'get',
headers: {
'Content-Type': 'application/json',
'x-company-id': `${
this.companyId || localStorage.getItem('companyId') || ''
}`,
Authorization: `Bearer ${localStorage.getItem('token')}`,
refreshToken: `Bearer ${localStorage.getItem('refresh')}`,
},
});
this.isConnected = true;
this.eventSource.retryInterval = 3000;
// Listen for messages
this.controller = this.eventSource.listen({
onMessage: (res: SseMessage) => {
try {
const message = res.data; // Attempt to parse JSON
this.zone.run(() => {
subject.next(new MessageEvent('message', { data: message }));
});
} catch (error) {
console.error('Failed to parse message:', res.data, error); // Log the raw data and error
}
},
// Log request errors
onRequestError: ({ request, error }) => {
console.error('[Request Error]', request, error);
this.reconnect(url, headers, subject);
},
// Handle response errors (e.g., server-side issues)
onResponseError: ({ response }) => {
console.error('[Response Error]', response.status, response.body);
if (response.status >= 400) {
this.reconnect(url, headers, subject);
}
},
});
return subject.asObservable();
}
close(): void {
if (this.eventSource) {
this.controller.abort();
this.eventSource = null;
this.isConnected = false;
console.log('SSE Connection closed');
}
}
/**
* Reconnects to the SSE endpoint after a delay.
* @param url - The URL for the SSE endpoint.
* @param headers - Optional headers for the request.
* @param subject - The subject to continue emitting events.
*/
private reconnect(
url: string,
headers: Record<string, string> | undefined,
subject: ReplaySubject<MessageEvent>
): void {
if (this.isConnected) return; // Prevent duplicate connections
console.log('Reconnecting to SSE in 1 second...');
setTimeout(() => {
this.zone.run(() => {
this.connect(url, headers).subscribe(subject);
});
}, 1000);
}
}
// To implement it,
const url = `https://example.com/v1/notification/subscribe`;
this.sseSubscription = this.sseService.connect(url).subscribe({
next: (event) => {
const data = JSON.parse(event.data);
// TOAST SERVICE
this.toast.success(data.data, data.timestamp);
},
error: (err) => {
console.error('SSE Error:', err);
},
});`
