64 lines
2.9 KiB
TypeScript
64 lines
2.9 KiB
TypeScript
// R-P8b (RA-C2) — real live board pub/sub. Uses the repo's existing in-process
|
|
// EventEmitter (`~/utils/stream/emitter.server`, the same one that already backs
|
|
// the workflow-execution SSE stream) so board mutations (stage move, bulk move,
|
|
// create, update, undo, archive) push real change-events to the connected
|
|
// pipeline-board SSE clients instead of only a snapshot + heartbeat. No new infra
|
|
// dependency is introduced.
|
|
//
|
|
// Scope caveat: this emitter is per web process. With a single web instance (the
|
|
// current deployment) every board client shares the process, so events reach all
|
|
// of them. Horizontal scaling of `hivegrid-web` would need a cross-process
|
|
// transport (e.g. Redis pub/sub) behind the same publish/subscribe API — tracked
|
|
// as a follow-up, out of this ticket's scope. This is server-only (.server) and
|
|
// is never imported by projectService.server (kept out of the pure-service test
|
|
// import graph) — publishing happens in the route-level orchestration helpers.
|
|
|
|
import { publishDashboardChange } from "~/services/dashboard/dashboardEvents.server";
|
|
import { emitter } from "~/utils/stream/emitter.server";
|
|
import type { ProjectBoardChange, ProjectBoardChangeType } from "./projectBoardEvents.shared";
|
|
|
|
/**
|
|
* In-process channel name for one pipeline board. Includes the tenantId as
|
|
* defence-in-depth even though pipeline ids are already globally unique — a
|
|
* subscriber only ever binds to its own tenant+pipeline channel.
|
|
*/
|
|
export function projectBoardChannel(tenantId: string, pipelineId: string): string {
|
|
return `projectBoard:${tenantId}:${pipelineId}`;
|
|
}
|
|
|
|
let listenerCapRaised = false;
|
|
|
|
/** Subscribe an SSE stream to a pipeline board channel. Returns an unsubscribe fn. */
|
|
export function subscribeToProjectBoard(tenantId: string, pipelineId: string, onChange: (payload: string) => void): () => void {
|
|
// Many concurrent board viewers can subscribe to the same channel; lift the
|
|
// default 10-listener cap so Node does not log a MaxListenersExceededWarning.
|
|
if (!listenerCapRaised) {
|
|
emitter.setMaxListeners(0);
|
|
listenerCapRaised = true;
|
|
}
|
|
const channel = projectBoardChannel(tenantId, pipelineId);
|
|
emitter.on(channel, onChange);
|
|
return () => {
|
|
emitter.off(channel, onChange);
|
|
};
|
|
}
|
|
|
|
/** Publish a board change so connected clients of this pipeline board revalidate. */
|
|
export function publishProjectBoardChange(input: {
|
|
tenantId: string;
|
|
pipelineId: string;
|
|
projectId?: string;
|
|
type: ProjectBoardChangeType;
|
|
}): void {
|
|
const payload: ProjectBoardChange = {
|
|
type: input.type,
|
|
pipelineId: input.pipelineId,
|
|
projectId: input.projectId,
|
|
at: new Date().toISOString(),
|
|
};
|
|
emitter.emit(projectBoardChannel(input.tenantId, input.pipelineId), JSON.stringify(payload));
|
|
// Project mutations also feed the tenant-wide dashboard stream (R-P11b):
|
|
// sales/project KPI widgets recompute their snapshot on these changes.
|
|
publishDashboardChange({ tenantId: input.tenantId, source: "projects" });
|
|
}
|