@nestjs/core
Version:
Nest - modern, fast, powerful node.js web framework (@core)
114 lines (113 loc) • 4.22 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SseStream = void 0;
const shared_utils_1 = require("@nestjs/common/utils/shared.utils");
const stream_1 = require("stream");
function toDataString(data) {
if ((0, shared_utils_1.isObject)(data)) {
return toDataString(JSON.stringify(data));
}
return data
.split(/\r\n|\r|\n/)
.map(line => `data: ${line}\n`)
.join('');
}
/**
* Adapted from https://raw.githubusercontent.com/EventSource/node-ssestream
* Transforms "messages" to W3C event stream content.
* See https://html.spec.whatwg.org/multipage/server-sent-events.html
* A message is an object with one or more of the following properties:
* - data (String or object, which gets turned into JSON)
* - type
* - id
* - retry
*
* If constructed with a HTTP Request, it will optimise the socket for streaming.
* If this stream is piped to an HTTP Response, it will set appropriate headers.
*/
class SseStream extends stream_1.Transform {
constructor(req) {
super({ objectMode: true });
this.lastEventId = null;
this._headersCommitted = false;
this._destination = null;
this._statusCode = 200;
if (req && req.socket) {
req.socket.setKeepAlive(true);
req.socket.setNoDelay(true);
req.socket.setTimeout(0);
}
}
get headersCommitted() {
return this._headersCommitted;
}
pipe(destination, options) {
this._destination = destination;
this._statusCode = options?.statusCode ?? 200;
this._additionalHeaders = options?.additionalHeaders;
return super.pipe(destination, options);
}
/**
* Writes SSE headers to the destination if they have not been sent yet.
* Headers are deferred until the first message so that, if the observable
* errors before any data is emitted, the HTTP status code can still be
* changed by an exception filter.
*/
commitHeaders() {
if (this._headersCommitted || !this._destination) {
return;
}
if (this._destination.writableEnded) {
return;
}
this._headersCommitted = true;
const statusCode = this._statusCode ?? 200;
const additionalHeaders = this._additionalHeaders;
if (this._destination.writeHead) {
this._destination.writeHead(statusCode, {
...additionalHeaders,
// See https://github.com/dunglas/mercure/blob/master/hub/subscribe.go#L124-L130
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
// Disable cache, even for old browsers and proxies
'Cache-Control': 'private, no-cache, no-store, must-revalidate, max-age=0, no-transform',
Pragma: 'no-cache',
Expire: '0',
// NGINX support https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-buffering
'X-Accel-Buffering': 'no',
});
this._destination.flushHeaders?.();
}
this._destination.write('\n');
}
_transform(message, encoding, callback) {
this.commitHeaders();
const sanitize = (val) => String(val).replace(/[\r\n]/g, '');
let data = message.type ? `event: ${sanitize(message.type)}\n` : '';
data +=
message.id !== undefined && message.id !== null
? `id: ${sanitize(message.id)}\n`
: '';
data += message.retry ? `retry: ${sanitize(message.retry)}\n` : '';
data += message.data ? toDataString(message.data) : '';
data += '\n';
this.push(data);
callback();
}
/**
* Calls `.write` but handles the drain if needed
*/
writeMessage(message, cb) {
if (message.id === undefined || message.id === null) {
this.lastEventId++;
message.id = this.lastEventId.toString();
}
if (!this.write(message, 'utf-8')) {
this.once('drain', cb);
}
else {
process.nextTick(cb);
}
}
}
exports.SseStream = SseStream;