49 lines
2.0 KiB
TypeScript
49 lines
2.0 KiB
TypeScript
// R-P11b — tenant-wide dashboard live pub/sub. Reuses the repo's existing
|
|
// in-process EventEmitter (`~/utils/stream/emitter.server`, the same one behind
|
|
// the workflow-execution and R-P8b pipeline-board SSE streams) so relevant
|
|
// mutations push a change signal to connected dashboard SSE clients, which then
|
|
// re-send a fresh permission-gated snapshot instead of staying frozen on the
|
|
// connect-time snapshot. No new infrastructure is introduced.
|
|
//
|
|
// Scope caveat (same as projectBoardEvents.server): the emitter is per web
|
|
// process. With a single web instance every dashboard client shares the
|
|
// process; horizontal scaling would need a cross-process transport (e.g. Redis
|
|
// pub/sub) behind the same publish/subscribe API — tracked follow-up, out of
|
|
// this ticket's scope.
|
|
|
|
import { emitter } from "~/utils/stream/emitter.server";
|
|
|
|
/** Sources that currently publish dashboard-relevant changes. */
|
|
export type DashboardChangeSource = "projects";
|
|
|
|
export interface DashboardChange {
|
|
readonly source: DashboardChangeSource;
|
|
readonly at: string;
|
|
}
|
|
|
|
/** In-process channel name for one tenant's dashboard change feed. */
|
|
export function dashboardChannel(tenantId: string): string {
|
|
return `dashboard:${tenantId}`;
|
|
}
|
|
|
|
let listenerCapRaised = false;
|
|
|
|
/** Subscribe an SSE stream to a tenant's dashboard changes. Returns an unsubscribe fn. */
|
|
export function subscribeToDashboardChanges(tenantId: string, onChange: (payload: string) => void): () => void {
|
|
if (!listenerCapRaised) {
|
|
emitter.setMaxListeners(0);
|
|
listenerCapRaised = true;
|
|
}
|
|
const channel = dashboardChannel(tenantId);
|
|
emitter.on(channel, onChange);
|
|
return () => {
|
|
emitter.off(channel, onChange);
|
|
};
|
|
}
|
|
|
|
/** Publish a dashboard-relevant change so connected clients receive a fresh snapshot. */
|
|
export function publishDashboardChange(input: { tenantId: string; source: DashboardChangeSource }): void {
|
|
const payload: DashboardChange = { source: input.source, at: new Date().toISOString() };
|
|
emitter.emit(dashboardChannel(input.tenantId), JSON.stringify(payload));
|
|
}
|