UNPKG

ngx-sse-client

Version:

A simple SSE (Server Sent Events) client for Angular applications.

184 lines (176 loc) 7.06 kB
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