u
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* after a successful mutation so other tabs/users can refetch.
|
||||
*/
|
||||
|
||||
/** @typedef {{ res: import('express').Response, companyId: number, userId: number, clientId: string|null, keepAlive: NodeJS.Timeout }} SseClient */
|
||||
/** @typedef {{ res: import('express').Response, companyId: number|null, userId: number, clientId: string|null, keepAlive: NodeJS.Timeout }} SseClient */
|
||||
|
||||
/** @type {Set<SseClient>} */
|
||||
const clients = new Set();
|
||||
@@ -13,16 +13,16 @@ const KEEP_ALIVE_MS = 20_000;
|
||||
/**
|
||||
* Register an open SSE response. Caller must write the initial headers.
|
||||
* @param {import('express').Response} res
|
||||
* @param {{ companyId: number, userId: number, clientId?: string|null }} meta
|
||||
* @param {{ companyId: number|null, userId: number, clientId?: string|null }} meta
|
||||
* @param {import('express').Request} [req]
|
||||
* @returns {() => void} unsubscribe
|
||||
*/
|
||||
export function subscribe(res, { companyId, userId, clientId }, req) {
|
||||
export function subscribe(res, { companyId = null, userId, clientId }, req) {
|
||||
const resolvedClientId = clientId || req?.headers?.['x-client-id'] || req?.query?.cid || null;
|
||||
/** @type {SseClient} */
|
||||
const client = {
|
||||
res,
|
||||
companyId,
|
||||
companyId: companyId ?? null,
|
||||
userId,
|
||||
clientId: resolvedClientId ? String(resolvedClientId) : null,
|
||||
keepAlive: setInterval(() => {
|
||||
@@ -42,6 +42,43 @@ export function subscribe(res, { companyId, userId, clientId }, req) {
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the active company scope for all open SSE streams of a user
|
||||
* (e.g. when the user selects or switches company on a pre-company stream)
|
||||
* and emits a `company` event to those streams.
|
||||
* @param {number} userId
|
||||
* @param {number|null} companyId
|
||||
*/
|
||||
export function setUserCompany(userId, companyId) {
|
||||
if (userId == null) return;
|
||||
const nextCompanyId = companyId ?? null;
|
||||
const frame = `event: company\ndata: ${JSON.stringify({ companyId: nextCompanyId })}\n\n`;
|
||||
for (const client of clients) {
|
||||
if (client.userId !== userId) continue;
|
||||
client.companyId = nextCompanyId;
|
||||
if (!client.res.writableEnded) {
|
||||
client.res.write(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a deleted company from any open SSE streams bound to it
|
||||
* and emits a `company` event (`{ companyId: null }`) to those streams.
|
||||
* @param {number} companyId
|
||||
*/
|
||||
export function clearDeletedCompany(companyId) {
|
||||
if (companyId == null) return;
|
||||
const frame = `event: company\ndata: ${JSON.stringify({ companyId: null })}\n\n`;
|
||||
for (const client of clients) {
|
||||
if (client.companyId !== companyId) continue;
|
||||
client.companyId = null;
|
||||
if (!client.res.writableEnded) {
|
||||
client.res.write(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End every open SSE stream so `server.close()` can finish (e.g. node --watch).
|
||||
* EventSource clients reconnect to the new process.
|
||||
@@ -76,10 +113,10 @@ export function hasActiveClient({ companyId, userId, clientId } = {}) {
|
||||
* If `targetClientId` or `targetUserId` is specified, sends only to matching clients of that user.
|
||||
*
|
||||
* @param {string} signal - SSE event name (e.g. `category`, `articles`, `modelconversation`)
|
||||
* @param {{ companyId: number, data?: string|object, targetClientId?: string, targetUserId?: number }} options
|
||||
* @param {{ companyId?: number|null, data?: string|object, targetClientId?: string, targetUserId?: number }} options
|
||||
*/
|
||||
export function emit(signal, { companyId, data, targetClientId, targetUserId }) {
|
||||
if (companyId == null) return;
|
||||
if (companyId == null && targetUserId == null) return;
|
||||
|
||||
const payload = data == null ? '{}' : (typeof data === 'string' ? data : JSON.stringify(data));
|
||||
const frame = `event: ${signal}\ndata: ${payload}\n\n`;
|
||||
@@ -88,7 +125,7 @@ export function emit(signal, { companyId, data, targetClientId, targetUserId })
|
||||
let sent = false;
|
||||
if (targetClientId) {
|
||||
for (const client of clients) {
|
||||
if (client.companyId !== companyId) continue;
|
||||
if (companyId != null && client.companyId !== companyId) continue;
|
||||
if (targetUserId != null && client.userId !== targetUserId) continue;
|
||||
if (client.clientId === targetClientId && !client.res.writableEnded) {
|
||||
client.res.write(frame);
|
||||
@@ -98,7 +135,7 @@ export function emit(signal, { companyId, data, targetClientId, targetUserId })
|
||||
}
|
||||
if (!sent && targetUserId != null) {
|
||||
for (const client of clients) {
|
||||
if (client.companyId !== companyId) continue;
|
||||
if (companyId != null && client.companyId !== companyId) continue;
|
||||
if (client.userId === targetUserId && !client.res.writableEnded) {
|
||||
client.res.write(frame);
|
||||
sent = true;
|
||||
|
||||
Reference in New Issue
Block a user