/** * @factory/realtime — comment-event → `comment.event` frame builder * (TASK-0094 R1). * * Pure (no sockets, no Redis — unit-testable): maps a `comment.*` domain * event from the `factory:events` stream to the shared * {@link CommentEventFrame} the server fans out over the page room. * * The API's CommentModule is the ONLY emitter (one service call → one * stream event; the frame is derived from it). Payload shape (the emitters * in `apps/api/src/modules/comments` include these fields — kept in * lockstep with the zod validation below): * * comment.created { workspaceId, commentId, pageId?, blockId?, recordId?, * replyId?, actorId, body } * comment.resolved { workspaceId, commentId, pageId?, actorId } * comment.reopened { workspaceId, commentId, pageId?, actorId } * comment.reaction { workspaceId, commentId, pageId?, actorId, emoji, active } * comment.deleted { workspaceId, commentId, pageId?, actorId } * * A frame is ONLY built when a `pageId` is present (the fan-out room is * `comment:{pageId}` — a comment with no resolvable page has no room; the * API's target resolver always resolves one for blocks/pages/records, so * this is defensive). */ import { z } from "zod"; import type { DomainEvent } from "@factory/shared"; import { CommentEventFrameSchema, type CommentEventFrame } from "@factory/shared/realtime/frames"; import { commentEventKindForEvent } from "@factory/shared/realtime/comment-events"; /** The preview length (the full comment is fetched on demand — R2). */ export const COMMENT_PREVIEW_MAX = 200; /** The comment stream-event payload (the fields the frame needs). */ const CommentEventPayloadSchema = z.object({ commentId: z.string().min(1), /** The fan-out room's page (the API resolves it from the target). */ pageId: z.string().min(1).optional(), /** The acting user (server-side — the client never supplies identity). */ actorId: z.string().min(1), /** New-thread / reply body (the preview source). */ body: z.string().optional(), /** A reply (a `comment.created` with a `replyId` → kind `replied`). */ replyId: z.string().optional(), }); type CommentEventPayload = z.infer; /** * Build the `comment.event` frame for a domain event. `null` when the event * does not map to a comment frame (another event name), the payload is * malformed, or no `pageId` is present (no room to fan out to). */ export function commentEventFrameFor(event: DomainEvent): CommentEventFrame | null { const kind = commentEventKindForEvent(event); if (kind === null) return null; const parsed = CommentEventPayloadSchema.safeParse(event.payload); if (!parsed.success) return null; const p = parsed.data as CommentEventPayload; if (p.pageId === undefined) return null; const preview = kind === "created" || kind === "replied" ? (p.body ?? "").slice(0, COMMENT_PREVIEW_MAX) : ""; const frame = CommentEventFrameSchema.safeParse({ type: "comment.event", kind, commentId: p.commentId, pageId: p.pageId, actorId: p.actorId, preview, }); return frame.success ? frame.data : null; }