u
This commit is contained in:
@@ -60,8 +60,11 @@ function corsMiddleware(req, res, next) {
|
|||||||
async function createApi() {
|
async function createApi() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use((_req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.setHeader('Cache-Control', 'no-store');
|
// Do not attach no-store to SSE endpoint
|
||||||
|
if (req.path !== '/api/events') {
|
||||||
|
res.setHeader('Cache-Control', 'no-store');
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.use(corsMiddleware);
|
app.use(corsMiddleware);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function register(app, basePath = '') {
|
|||||||
* es.addEventListener('article-42000045', () => { refetchArticle('42000045'); });
|
* es.addEventListener('article-42000045', () => { refetchArticle('42000045'); });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* Keep-alive comments (`: keepalive`) are sent roughly every 25s.
|
* Keep-alive comments (`: heartbeat`) are sent roughly every 20s.
|
||||||
* Reconnect after changing the selected company (the stream is bound
|
* Reconnect after changing the selected company (the stream is bound
|
||||||
* to the company at connect time).
|
* to the company at connect time).
|
||||||
*
|
*
|
||||||
@@ -76,24 +76,28 @@ export function register(app, basePath = '') {
|
|||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/Error'
|
* $ref: '#/components/schemas/Error'
|
||||||
*/
|
*/
|
||||||
app.get(`${basePath}/events`, (req, res) => {
|
app.get(`${basePath}/events`, handleSse);
|
||||||
const companyId = req.user.selectedCompanyId;
|
}
|
||||||
if (companyId == null) {
|
|
||||||
return res.status(400).json({ error: 'No company selected' });
|
export function handleSse(req, res) {
|
||||||
}
|
const companyId = req.user?.selectedCompanyId;
|
||||||
|
if (companyId == null) {
|
||||||
req.socket.setTimeout(0);
|
return res.status(400).json({ error: 'No company selected' });
|
||||||
res.setTimeout(0);
|
}
|
||||||
|
|
||||||
res.status(200);
|
req.socket?.setTimeout(0);
|
||||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
res.setTimeout?.(0);
|
||||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
||||||
res.setHeader('Connection', 'keep-alive');
|
res.writeHead(200, {
|
||||||
res.setHeader('X-Accel-Buffering', 'no');
|
'Content-Type': 'text/event-stream',
|
||||||
res.flushHeaders?.();
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
res.write(': connected\n\n');
|
'X-Accel-Buffering': 'no' // Instructs NGINX explicitly to disable buffering
|
||||||
|
});
|
||||||
subscribe(res, { companyId, userId: req.user.id });
|
res.flushHeaders(); // Ensure headers are sent over HTTP/2 frame immediately
|
||||||
});
|
|
||||||
|
// Send an initial ping to establish frame
|
||||||
|
res.write(': ping\n\n');
|
||||||
|
|
||||||
|
subscribe(res, { companyId, userId: req.user?.id }, req);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,16 @@
|
|||||||
/** @type {Set<SseClient>} */
|
/** @type {Set<SseClient>} */
|
||||||
const clients = new Set();
|
const clients = new Set();
|
||||||
|
|
||||||
const KEEP_ALIVE_MS = 25_000;
|
const KEEP_ALIVE_MS = 20_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an open SSE response. Caller must write the initial headers.
|
* Register an open SSE response. Caller must write the initial headers.
|
||||||
* @param {import('express').Response} res
|
* @param {import('express').Response} res
|
||||||
* @param {{ companyId: number, userId: number }} meta
|
* @param {{ companyId: number, userId: number }} meta
|
||||||
|
* @param {import('express').Request} [req]
|
||||||
* @returns {() => void} unsubscribe
|
* @returns {() => void} unsubscribe
|
||||||
*/
|
*/
|
||||||
export function subscribe(res, { companyId, userId }) {
|
export function subscribe(res, { companyId, userId }, req) {
|
||||||
/** @type {SseClient} */
|
/** @type {SseClient} */
|
||||||
const client = {
|
const client = {
|
||||||
res,
|
res,
|
||||||
@@ -24,7 +25,7 @@ export function subscribe(res, { companyId, userId }) {
|
|||||||
userId,
|
userId,
|
||||||
keepAlive: setInterval(() => {
|
keepAlive: setInterval(() => {
|
||||||
if (res.writableEnded) return;
|
if (res.writableEnded) return;
|
||||||
res.write(': keepalive\n\n');
|
res.write(': heartbeat\n\n');
|
||||||
}, KEEP_ALIVE_MS),
|
}, KEEP_ALIVE_MS),
|
||||||
};
|
};
|
||||||
clients.add(client);
|
clients.add(client);
|
||||||
@@ -35,6 +36,7 @@ export function subscribe(res, { companyId, userId }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
res.on('close', unsubscribe);
|
res.on('close', unsubscribe);
|
||||||
|
req?.on('close', unsubscribe);
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user