Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/integrations/projects/projectBoardEvents.server.ts
T

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" });
}