diff --git a/README.md b/README.md
index 44591f8..01f9b1d 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,15 @@ interface ISseMiddlewareOptions {
* @default 5000
*/
keepAliveInterval?: number;
+
+ /**
+ * If you are using expressjs/compression, you MUST set this option to true.
+ * It will call res.flush() after each SSE messages so the partial content is compressed and reaches the client.
+ * Read {@link https://github.com/expressjs/compression#server-sent-events} for more.
+ *
+ * @default false
+ */
+ flushAfterWrite: boolean;
}
```
@@ -231,34 +240,16 @@ app.get('/events', sse({ serializer: data => data.toString() }), yourMiddleware)
### Using Compression
-If you are using a HTTP compression middleware, like [expressjs/compression](https://github.com/expressjs/compression), _expresse_ won't likely work out of the box.
+If you are using a dynamic HTTP compression middleware, like [expressjs/compression](https://github.com/expressjs/compression), _expresse_ won't likely work out of the box.
This is due to the nature of compression and how compression middlewares work. For example, express' compression middleware will patch res.write and hold the content written in it until `res.end()` or an equivalent is called. Then the body compression can happen and the compressed content can be sent.
-However `res.write()` must not be buffered with SSEs.
-You have two main options:
-
-
-Disable compression on SSE endpoints (preferred)
-
-There are chances your SSE messages are short and won't benefit from compression. Moreover, compression is not efficient on short messages.
-
-You have various ways to disable compression (conditional middleware, per-route compression, etc), here's an example for expressjs/compression, that allows to filter the routes that will benefit from compression:
+Therefore, `res.write()` must not be buffered with SSEs. That's why ExpreSSE offers expressjs/compression support through the `flushAfterWrite` option. It **must** be set when using the compression middleware:
```typescript
-app.use(compression({
- level: 7,
- filter(req, res) {
- return !req.originalUrl.includes('/your-sse-path');
- }
-}));
-```
-
-
-
-Call res.flush() to send compressed events (expressjs/compression -specific)
+app.use(compression());
-If you want to keep compression, that's still possible, expressjs/compression provides a [`res.flush()`](https://expressjs.com/en/resources/middleware/compression.html#resflush) function that will compress and send the (partial) response content immediately (an event), instead of buffering everything that comes until the response end.
-
-After each `res.sse.*()` call, also call `res.flush()`. That's it. Oh and add a comment that links to this paragraph.
-
+app.get('/events', sse({ flushAfterWrite: true }), (req, res: ISseResponse) => {
+ res.sse.comment('Welcome! This is a compressed SSE stream.');
+});
+```
diff --git a/src/sse_handler_middleware.ts b/src/sse_handler_middleware.ts
index 424f2d8..d1a6741 100644
--- a/src/sse_handler_middleware.ts
+++ b/src/sse_handler_middleware.ts
@@ -1,4 +1,4 @@
-import { Handler } from 'express';
+import { Handler, Response } from 'express';
import * as fmt from './sse_formatter';
export interface ISseMiddlewareOptions {
@@ -16,10 +16,25 @@ export interface ISseMiddlewareOptions {
* @default 5000
*/
keepAliveInterval: number;
+
+ /**
+ * If you are using expressjs/compression, you MUST set this option to true.
+ * It will call res.flush() after each SSE messages so the partial content is compressed and reaches the client.
+ * Read {@link https://github.com/expressjs/compression#server-sent-events} for more.
+ *
+ * @default false
+ */
+ flushAfterWrite: boolean;
+}
+
+export const sseWrite = Symbol('@toverux/expresse#sseWrite');
+
+export interface ISseHandlerResponse extends Response {
+ [sseWrite]: (chunk: any) => void;
}
export function sseHandler(options: Partial = {}): Handler {
- const { keepAliveInterval = 5000 } = options;
+ const { keepAliveInterval = 5000, flushAfterWrite = false } = options;
return (req, res, next) => {
//=> Basic headers for an SSE session
@@ -32,16 +47,27 @@ export function sseHandler(options: Partial = {}): Handle
//=> Write immediately on the socket.
// This has the advantage to 'test' the connection: if the client can't access this resource because of
// CORS restrictions, the connection will fail instantly.
- res.write(': sse-start\n');
+ write(': sse-start\n');
//=> Regularly send keep-alive SSE comments, clear interval on socket close
- const keepAliveTimer = setInterval(() => res.write(': sse-keep-alive\n'), keepAliveInterval);
+ const keepAliveTimer = setInterval(() => write(': sse-keep-alive\n'), keepAliveInterval);
//=> When the connection gets closed (close=client, finish=server), stop the keep-alive timer
res.once('close', () => clearInterval(keepAliveTimer));
res.once('finish', () => clearInterval(keepAliveTimer));
- //=> Done
+ //=> Attach the res.write wrapper function to the response for internal use
+ (res as ISseHandlerResponse)[sseWrite] = write;
+
+ //=> Done.
next();
+
+ /**
+ * An internal function to write on the response socket with respect to compression settings.
+ */
+ function write(chunk: any) {
+ res.write(chunk);
+ flushAfterWrite && (res as any).flush();
+ }
};
}
diff --git a/src/sse_middleware.ts b/src/sse_middleware.ts
index 3b74fda..ce7aa62 100644
--- a/src/sse_middleware.ts
+++ b/src/sse_middleware.ts
@@ -1,7 +1,7 @@
import { compose } from 'compose-middleware';
import { Handler, NextFunction, Request, Response } from 'express';
import * as fmt from './sse_formatter';
-import { ISseMiddlewareOptions, sseHandler } from './sse_handler_middleware';
+import { ISseHandlerResponse, ISseMiddlewareOptions, sseHandler, sseWrite } from './sse_handler_middleware';
export interface ISseFunctions {
/**
@@ -57,16 +57,18 @@ export function sse(options: Partial = {}): Handler {
const { serializer } = options;
function middleware(req: Request, res: Response, next: NextFunction): void {
+ const write = (res as ISseHandlerResponse)[sseWrite];
+
//=> Install the sse*() functions on Express' Response
(res as ISseResponse).sse = {
data(data: fmt.SseValue, id?: string) {
- res.write(fmt.message(null, data, id, serializer));
+ write(fmt.message(null, data, id, serializer));
},
event(event: string, data: fmt.SseValue, id?: string) {
- res.write(fmt.message(event, data, id, serializer));
+ write(fmt.message(event, data, id, serializer));
},
comment(comment: string) {
- res.write(fmt.comment(comment));
+ write(fmt.comment(comment));
}
};