Let's start with the Angular Side of our solution
The following two code snippets are part of the same Angular services. Here the function connect
creates a new EventSource and forwards all the messages it receives from the backend API into an observer.
public eventSource; public connect(url): Observable<any> { return Observable.create((observer) => { const es = new EventSource(url); this.eventSource = es; es.onmessage = (event) => { this._zone.run(() => observer.next( event.data )); }; es.onerror = (error) => { observer.error(error); }; }); }
Next I create two arbitrary abbreviations EOS
(end of stream) and BOS
(beginning of stream), not really necessary to be honest, but sometimes useful, especially if the back-end is running long running queries. By sending the BOS
immediately you are making the client to receive the response headers on the moment of the request.
Then I combine the data in the messages and use an old trick to trigger a download (creating an html element, and clicking it).
private beginningOfStream: string = "BOS" private endOfStream: string = "EOS" public async stream(url:string): Promise<any> { const filename = `export_${moment().format('L').replace(/\//g, "")}_${moment().format('HHmm')}.csv`; return new Promise(async (resolve, reject) => { try { let data = ''; this.connect(url).subscribe((response) => { switch (response) { case this.beginningOfStream: break; case this.endOfStream: const blob = new Blob([data], { type: 'application/txt' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); this.end(); resolve({ info: 'success' }); break; default: data += JSON.parse(response); } }, (error) => { if (this.eventSource) { this.eventSource.close(); } reject(error); }); } catch (error) { console.log(`Error occurred: ${error}`); if (this.eventSource) { this.eventSource.close(); } reject(error); } }); }
and finish with the Node Side of our solution
This is my sample Express Route. Now I the way I use the combination of Express+Typescript is slightly awkward but it works nicely. Maybe that will make another good post.
But, at the end of the day, it's quite obvious what I am trying to achieve.
I am creating the headers of the event-stream and I am sending messages back to the client, by keeping the connection alive.
export class DataRoute { public router = Router() as Router; constructor() { this.router.use((req, res: any, next) => { const successs = 200; res.sseSetup = () => { res.writeHead(successs, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); res.connection.setTimeout(0); }; res.sseSend = (data: any) => { res.write("data: " + data + "\n\n", "utf8", () => { if (res.flushHeaders) { res.flushHeaders(); } }); }; next(); }); this.router.get("/endpoint", (req, res, next) => { const fileName = `export${moment().format("L").replace(/\//g, "-")}.csv`; res.setHeader("Content-disposition", `attachment; filename=${fileName}`); res["sseSetup"](); res["sseSend"]("BOS"); data.forEach(function (element) { res["sseSend"](JSON.stringify(element)); }); this.closeStream(res); }); } private closeStream(res: any) { res.sseSend("EOS"); res.end(); } }
Top comments (0)