1/** 2 * @factory/shared/realtime/comment-events — the comment stream-event → 3 * `comment.event` frame-kind mapping (TASK-0094 R1). 4 * 5 * Two layers, one emission (recorded in the task file): 6 * - the PUBLIC stream events on `factory:events` (the Phase-1 catalog: 7 * `comment.created` / `comment.resolved` — Phase-13 webhooks listen here); 8 * - the ROOM-INTERNAL `comment.event` frame, whose `kind` set is 9 * `created | replied | resolved | reopened | reacted | deleted` (the 10 * comment panel's full reactivity). 11 * 12 * The API's CommentModule emits ONE event per operation; this mapping 13 * derives the frame kind from the event name + payload: 14 * - `comment.created` → `replied` when the payload carries a 15 * `replyId` (a reply is a comment with a parent — the API emits 16 * `comment.created` for replies too, the catalog shape), else `created`. 17 * - `comment.reopened` → `reopened` (additive internal event) 18 * - `comment.reaction` → `reacted` (additive internal event) 19 * - `comment.deleted` → `deleted` (additive internal event) 20 * - `comment.resolved` → `resolved` (catalog event) 21 * 22 * The realtime server (the `comment-notification-source`) is the ONLY 23 * consumer of this table; both the API (emitter) and the realtime app 24 * (mapper) import it from here so the two layers cannot drift. 25 */ 26 27import type { DomainEvent } from "../events.js"; 28 29/** The `comment.event` frame kinds (the frame schema's enum — lockstep). */ 30export const COMMENT_EVENT_KINDS = [ 31 "created", 32 "replied", 33 "resolved", 34 "reopened", 35 "reacted", 36 "deleted", 37] as const; 38export type CommentEventKind = (typeof COMMENT_EVENT_KINDS)[number]; 39 40/** The stream event names that map to a comment frame (single source). */ 41export const COMMENT_EVENT_STREAM_NAMES = [ 42 "comment.created", 43 "comment.resolved", 44 "comment.reopened", 45 "comment.reaction", 46 "comment.deleted", 47] as const; 48export type CommentEventStreamName = (typeof COMMENT_EVENT_STREAM_NAMES)[number]; 49 50/** 51 * Derive the `comment.event` frame kind from a stream event envelope. 52 * `null` when the event does not map to a comment frame (any other event 53 * name, or a malformed payload). 54 * 55 * The reply case is payload-driven: the API emits `comment.created` for 56 * BOTH a new thread and a reply (a reply = a comment with a parent); the 57 * reply payload carries `replyId` (the reply row id) + `commentId` (the 58 * parent thread id). A `comment.created` WITHOUT a `replyId` is a new 59 * thread → `created`. 60 */ 61export function commentEventKindForEvent(event: DomainEvent): CommentEventKind | null { 62 const payload = event.payload as Record<string, unknown> | null | undefined; 63 switch (event.name) { 64 case "comment.created": { 65 // A reply is a `comment.created` whose payload carries a `replyId`. 66 return payload && typeof payload.replyId === "string" ? "replied" : "created"; 67 } 68 case "comment.resolved": 69 return "resolved"; 70 case "comment.reopened": 71 return "reopened"; 72 case "comment.reaction": 73 return "reacted"; 74 case "comment.deleted": 75 return "deleted"; 76 default: 77 return null; 78 } 79} 80