ngx-sse-client
Version:
A simple SSE (Server Sent Events) client for Angular applications.
184 lines (176 loc) • 7.06 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable } from '@angular/core';
import * as i1 from '@angular/common/http';
import { HttpEventType } from '@angular/common/http';
import { Observable } from 'rxjs';
import { repeatWhen, retryWhen, takeWhile, delay, tap } from 'rxjs/operators';
const defaultSseOptions = { keepAlive: true, reconnectionDelay: 3_000, responseType: 'event' };
const defaultRequestOptions = { observe: 'events', reportProgress: true, responseType: 'text' };
class SseClientSubscriber {
httpClient;
sseOptions;
httpClientOptions;
url;
method;
static SEPARATOR = ':';
progress = 0;
chunk = '';
constructor(httpClient, sseOptions, httpClientOptions, url, method) {
this.httpClient = httpClient;
this.sseOptions = sseOptions;
this.httpClientOptions = httpClientOptions;
this.url = url;
this.method = method;
}
createObservable() {
return new Observable((observer) => {
const subscription = this.subscribeStreamRequest(this.url, this.sseOptions, this.httpClientOptions, this.method, observer);
return () => subscription.unsubscribe();
});
}
subscribeStreamRequest(url, options, requestOptions, method, observer) {
return this.httpClient
.request(method, url, requestOptions)
.pipe(repeatWhen((completed) => this.repeatWhen(completed, options.keepAlive, options.reconnectionDelay)))
.pipe(retryWhen((error) => this.retryWhen(error, options.keepAlive, options.reconnectionDelay, observer)))
.subscribe((event) => this.parseStreamEvent(event, observer));
}
repeatWhen(completed, keepAlive, reconnectionDelay) {
return completed.pipe(takeWhile(() => keepAlive)).pipe(delay(reconnectionDelay));
}
retryWhen(attempts, keepAlive, reconnectionDelay, observer) {
return attempts
.pipe(tap((error) => this.threatRequestError(error, observer)))
.pipe(takeWhile(() => keepAlive))
.pipe(delay(reconnectionDelay));
}
threatRequestError(event, observer) {
this.dispatchStreamData(this.errorEvent(event), observer);
if (!this.isValidStatus(event.status)) {
observer.error(event);
}
}
isValidStatus(status) {
return status !== undefined && status !== null && status <= 299;
}
parseStreamEvent(event, observer) {
if (event.type === HttpEventType.Sent) {
this.progress = 0;
return;
}
if (event.type === HttpEventType.DownloadProgress) {
this.onStreamProgress(event.partialText, observer);
return;
}
if (event.type === HttpEventType.Response) {
this.onStreamCompleted(event, observer);
return;
}
}
onStreamProgress(data, observer) {
data = data.substring(this.progress);
this.progress += data.length;
data.split(/(\r\n|\r|\n){2}/g).forEach((part) => this.parseEventData(part, observer));
}
onStreamCompleted(response, observer) {
this.onStreamProgress(response.body, observer);
this.dispatchStreamData(this.parseEventChunk(this.chunk), observer);
this.chunk = '';
this.progress = 0;
if (this.sseOptions.keepAlive) {
const message = `Server response ended, will reconnect in ${this.sseOptions.reconnectionDelay}ms`;
this.dispatchStreamData(this.errorEvent({ status: 1, message }), observer);
}
else {
observer.complete();
}
}
parseEventData(part, observer) {
if (part.trim().length === 0) {
this.dispatchStreamData(this.parseEventChunk(this.chunk), observer);
this.chunk = '';
}
else {
this.chunk += part;
}
}
parseEventChunk(chunk) {
if (!chunk || chunk.length === 0)
return;
const chunkEvent = { id: undefined, data: '', event: 'message' };
chunk.split(/\n|\r\n|\r/).forEach((line) => this.parseChunkLine(line.trim(), chunkEvent));
return this.messageEvent(chunkEvent.event, { lastEventId: chunkEvent.id, data: chunkEvent.data });
}
parseChunkLine(line, event) {
const index = line.indexOf(SseClientSubscriber.SEPARATOR);
if (index <= 0)
return;
const field = line.substring(0, index);
if (Object.keys(event).findIndex((key) => key === field) === -1)
return;
let data = line.substring(index + 1).replace(/^\s/, '');
if (field === 'data')
data = event.data + data;
event[field] = data;
}
dispatchStreamData(event, observer) {
if (!this.validEvent(event))
return;
if (this.sseOptions.responseType === 'event') {
observer.next(event);
}
else {
observer.next(event.data);
}
}
validEvent(event) {
if (!event)
return false;
if (event.type === 'error' && this.sseOptions.responseType !== 'event')
return false;
if (event.type !== 'error' && (!event.data || !event.data.length))
return false;
return true;
}
messageEvent(type, options) {
return new MessageEvent(type, options);
}
errorEvent(error) {
let eventData;
if (error && error.status > 0) {
eventData = { error, message: error.message };
if (!this.isValidStatus(error.status)) {
eventData['status'] = error.status;
eventData['statusText'] = error.statusText;
}
}
return new ErrorEvent('error', eventData);
}
}
class SseClient {
httpClient;
constructor(httpClient) {
this.httpClient = httpClient;
}
stream(url, options, requestOptions, method = 'GET') {
var sseOptions = Object.assign({}, defaultSseOptions, options);
var httpClientOptions = Object.assign({}, requestOptions, defaultRequestOptions);
return new SseClientSubscriber(this.httpClient, sseOptions, httpClientOptions, url, method).createObservable();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: SseClient, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: SseClient, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: SseClient, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i1.HttpClient }] });
/*
* Public API Surface of ngx-sse-client
*/
/**
* Generated bundle index. Do not edit.
*/
export { SseClient, defaultRequestOptions, defaultSseOptions };
//# sourceMappingURL=ngx-sse-client.mjs.map