Next.js 14 + Drizzle/PostgreSQL + SSE realtime sync — highlight, freehand, comment, bookmark + ฟีเจอร์ต่อยอด F1–F19 (edit/move, undo/redo, shapes, redaction, export PDF/CSV, reply, resolve, search, summary, textbox, stamp, version history) Co-Authored-By: Claude Code <noreply@anthropic.com>
3.6 KiB
3.6 KiB
| paths | |||
|---|---|---|---|
|
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 อื่นไม่เห็น - 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.signalabort /cancel) - Dedupe ด้วย id: event ที่ id มีอยู่ใน state แล้ว ไม่ต้อง append ซ้ำ — ทำงานคู่กับ
useOptimisticใน PDFViewer: SSE event อาจมาถึงก่อน action เสร็จ → duplicate ชั่วขณะที่หายเองเมื่อ optimistic 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