eCourt-note/.claude/rules/realtime.md
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 6ddc35d6d5 เลือก annotation บนหน้า PDF → sidebar ตามไปเน้นรายการนั้น + สร้างเสร็จเลือกให้ทันที + กัน duplicate ชั่วขณะตอนสร้าง: (1) ส่ง selectedId ให้ AnnotationSidebar — ไฮไลต์แถว (ทั้งลิสต์หลักและผลค้นหา) + scrollIntoView block:nearest + สลับมาแท็บ Annotations ให้เองถ้ากำลังอยู่ Bookmarks (rAF รอแถว render หลังสลับแท็บค่อยเลื่อน) (2) AUTO_SELECT_TYPES — สร้าง DRAW/SHAPE/TEXTBOX/STAMP/HIGHLIGHT/REDACTION เสร็จ = เลือกตัวใหม่ทันทีด้วย saved.id (กรอบ selection + sidebar ตามไปเลย; COMMENT ยกเว้นเพราะ composer/pin ทำหน้าที่นั้นอยู่แล้ว) — แก้ปัญหา "วาดเสร็จคลิกที่เส้น tool ยังเป็นวาด sidebar นิ่ง" (3) กันอาการ row โผล่แล้วหาย (0→2→1): เดิม optimistic row กับ row จาก server คนละ id ทำให้ SSE event มาก่อน action เสร็จ dedupe ไม่ได้ — ตอนนี้ client ส่ง id ของ optimistic row ให้ createAnnotation ใช้ซ้ำ (validate UUID, ไม่ส่ง = สุ่มเองตามเดิมสำหรับ redo path) ทุกชั้น dedupe ด้วย id เดียวกันตั้งแต่แรก — sync PLAN.md §4 + rules/realtime.md ให้ตรงพฤติกรรมใหม่
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-14 11:35:05 +07:00

4.2 KiB

paths
hooks/**
app/api/**
lib/annotate-events.ts

Realtime Rules (รายละเอียด: docs/PLAN.md §4)

  • Sync ใช้ SSE: hook เปิด EventSource ไป /api/annotations/stream?fileId=<uuid> และ es.close() เมื่อ unmount
  • EventSource ต่อใหม่อัตโนมัติเมื่อ connection ขาด — ห้ามทำ retry logic เอง
  • Event payload เป็น DTO (camelCase + ISO string) จาก server ตรง ๆ — ไม่มีการแปลง snake_case
  • Server Actions ต้อง publish ผ่าน lib/annotate-events.ts หลังเขียน DB สำเร็จทุกครั้ง (create/update/delete ของ annotations, create/delete ของ replies, create/update/delete ของ bookmarks — update ของ bookmarks ใช้กับแก้ชื่อ/โน้ต/ย้ายหัวข้อ และลูกที่ถูกยกขึ้น root ตอนแม่ถูกลบ, client replace by id) — ลืม publish = client อื่นไม่เห็น; คู่กันในจุดเดียวกันต้อง revalidatePath หน้า viewer+summary (revalidateFilePages) มิฉะนั้นกลับเข้าหน้าใหม่ (back → คลิกไฟล์) ได้ RSC payload เก่าจาก Router Cache ฝั่ง client จนต้อง hard refresh
  • Event update (annotations): client replace ตาม id — idempotent (มาก่อน/หลัง optimistic ก็ได้ผลเดียวกัน) ห้ามเพิ่ม logic กัน duplicate เกินกว่า replace ด้วย id (PLAN-FEAT.md §5) — update row ของ F10 (setAnnotationResolved) ใช้ channel เดียวกันนี้ (row เต็มมี resolved/resolvedAt)
  • Event ของ replies (F9): create → append + dedupe ด้วย id, delete → filter ตาม id — และเมื่อ annotation แม่ถูกลบ ต้องเคลียร์ replies ของแม่ใน state ฝั่ง client ด้วย (CASCADE ใน DB ไม่ส่ง event ราย reply)
  • restoreVersion (F19) ก็ publish create event ต่อรายการที่ insert สำเร็จเหมือน action เขียนอื่น ๆ — แท็บของผู้กดเองก็ได้ event ผ่าน SSE ของตัวเอง จึงไม่ต้อง sync state ซ้ำใน client
  • SSE route: ต้อง cleanup (unsubscribe + close) เมื่อ client ตัดการเชื่อมต่อ (req.signal abort / cancel)
  • Dedupe ด้วย id: event ที่ id มีอยู่ใน state แล้ว ไม่ต้อง append ซ้ำ — ทำงานคู่กับ useOptimistic ใน PDFViewer: SSE event อาจมาถึงก่อน action เสร็จ — createAnnotation รับ id ที่ client สร้างของ optimistic row มาใช้ซ้ำ (validate เป็น UUID) เพื่อให้ dedupe ด้วย id เดียวกันตั้งแต่แรก ไม่เกิด duplicate ชั่วขณะ (row โผล่แล้วหาย) — อย่าเพิ่ม logic กัน duplicate เกินกว่า dedupe ด้วย id
  • pub/sub เป็น in-memory ของ process เดียว — ถ้ามีวันที่ต้องรันหลาย instance ให้เพิ่มตัวกลาง (Redis/LISTEN-NOTIFY) ก่อน อย่าแอบ sync ผ่าน DB polling
  • Map ของ listener ใน lib/annotate-events.ts ต้องเก็บบน globalThis — module-level state จะถูก duplicate เป็น 2 อินสแตนซ์ใน Next dev (bundle ของ "use server" กับ route handler แยกกัน) แล้ว event ไม่ไหลถึงกัน — ห้ามเปลี่ยนกลับเป็น module-level Map