การใช้เนื้อหา › Realtime
เริ่มต้น
เนื้อหา
Plugins
การดูแลระบบ
การ Deploy
สูตรสำเร็จ
Realtime
หน้านี้สอนอะไร
ทำให้รายการหรือเอกสารบนเว็บอัปเดตตามที่ผู้แก้บันทึก โดยไม่ต้องโหลดหน้าใหม่: subscribe การเปลี่ยนแปลงของ collection ผ่าน Server-Sent Events
ควรอ่านก่อน: การควบคุมสิทธิ์, ฉบับร่าง เวอร์ชัน และการตั้งเวลา
เปิดทีละ collection:
{
slug: 'posts',
realtime: true,
fields: [/* … */],
}จากนั้นทุกการบันทึกบทความจะเขียนแถวในตาราง realtime-events ด้วย ใน transaction เดียวกัน
สร้าง migration ให้ตารางนี้ (easy-cms migrate:create realtime) ตารางมีเฉพาะเมื่อมีอย่างน้อยหนึ่ง
collection ที่ตั้ง realtime: true
ผู้ subscribe ได้การเปลี่ยนแปลงห้าแบบ: create, update, delete, publish และ unpublish
แต่ละอันมีเอกสารตามที่ผู้ subscribe คนนั้นอ่านได้ผ่าน REST: สิทธิ์อ่านของ collection (รวม where
ระดับแถว), สิทธิ์อ่านระดับ field, สิทธิ์ของ API key และฉบับร่าง ใช้กับแต่ละคนแยกกัน
ในเบราว์เซอร์
createClient() จาก @easy-cms/core/client มี live(): รายการที่โหลดด้วย find แล้วอัปเดตตาม
event ต่อไป ถ้าใส่ id จะเป็นเอกสารเดียว
Next.js
useLive จาก @easy-cms/next/live ใน Client Component:
'use client'
import { createClient } from '@easy-cms/core/client'
import { useLive } from '@easy-cms/next/live'
import type config from '@/easy-cms.config'
const cms = createClient<typeof config>({ url: '/api/cms' })
export function LivePosts() {
const { docs, ready, error } = useLive(cms, 'posts', { sort: '-createdAt', limit: 20 })
if (!ready) return <p>กำลังโหลด…</p>
return (
<ul>
{docs.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}ส่งสิ่งที่ Server Component โหลดมาเป็นอาร์กิวเมนต์ที่สี่ (initial) เพื่อแสดงก่อนโหลดครั้งแรกเสร็จ
ตัวอย่าง Next.js มีหน้านี้ที่ /live
Nuxt
useLive ถูก auto-import คืนค่าเป็น ref และถ้า query เป็น reactive จะ subscribe ใหม่เมื่อเปลี่ยน:
<script setup lang="ts">
import { createClient } from '@easy-cms/core/client'
import type config from '~/easy-cms.config'
const cms = createClient<typeof config>({ url: '/api/cms' })
const { docs: posts, ready } = useLive(cms, 'posts', { sort: '-createdAt', limit: 20 })
</script>รายการทำอะไร
create,update,publishและunpublishใส่เอกสารเข้าไป (หรือแทนอันเดิม) ตามลำดับของsort(ค่าเริ่มต้น-createdAt) ถ้ามีlimitจะเก็บไม่เกินจำนวนนั้นdeleteและleaveเอาออกleaveแปลว่าผู้ subscribe คนนี้มองไม่เห็นเอกสารนั้นแล้ว เช่น บทความที่ผู้เยี่ยมชมเห็นถูก unpublish หรือไม่ตรงwhereหรือสิทธิ์ของผู้ใช้แล้ว ทั้งสองมาเฉพาะเอกสารที่เคยเห็น- หลัง
resetจะโหลดรายการใหม่
Subscribe เอง
subscribe() รับหลาย subscription ในการเชื่อมต่อเดียว: ทั้ง collection, เอกสารที่ตรง where หรือ
เอกสารเดียว
const cms = createClient<typeof config>({ url: 'https://cms.example.com/api/cms' })
const stream = cms.subscribe(
[
{ collection: 'posts', where: { category: { equals: 'news' } } },
{ collection: 'comments', id: 42 },
],
{
onEvent: (change) => {
if (change.collection === 'posts' && 'doc' in change) console.log(change.doc.title)
},
onReady: () => console.log('เชื่อมต่อแล้ว'),
onReset: () => reloadEverything(),
onError: (error) => console.warn(error),
},
)
// ภายหลัง:
stream.close()อ่าน stream ด้วย fetch จึงใช้ session cookie ของเบราว์เซอร์และ token ของ client
(Authorization: Bearer) ได้ เชื่อมต่อใหม่เองแบบเว้นระยะเพิ่มขึ้น และต่อจากที่ค้างไว้ หยุดเมื่อ API
ปฏิเสธ subscription (400, 401, 403 หรือ 404)
subscription ของ stream เปลี่ยนไม่ได้ ถ้าจะเปลี่ยนให้ปิดแล้ว subscribe ใหม่ต่อจากที่ stream เดิมไปถึง จะไม่พลาดอะไรระหว่างนั้น:
const next = cms.subscribe(newSubscriptions, handlers, { lastEventId: stream.lastEventId })
stream.close()Stream
GET <api>/realtime?s=<subscription>[&s=…] ตอบเป็น text/event-stream แต่ละ s คือ path ใต้ API
พร้อม query แบบเดียวกับการอ่านผ่าน REST เข้ารหัส URL เป็นพารามิเตอร์เดียว:
s | ติดตาม |
|---|---|
posts | ทุกบทความที่ผู้ subscribe อ่านได้ |
posts/12 | บทความ 12 |
posts?where[category][equals]=news | บทความที่ตรง where (แบบวงเล็บหรือ JSON where= เหมือนรายการ) |
posts?draft=true&depth=0&locale=th | รวมฉบับร่าง (เฉพาะทีมงาน เหมือน REST), depth และ locale |
ได้ไม่เกิน 20 ต่อ stream ถ้าจะเปลี่ยนให้เปิด stream ใหม่
id: 812
event: update
data: {"collection":"posts","id":12,"doc":{"id":12,"title":"Hello","status":"published"}}
id: 813
event: delete
data: {"collection":"posts","id":9}id:คือ id ของ event และ event ส่งตามลำดับนี้ client ที่เชื่อมต่อใหม่ส่งค่านี้เป็นLast-Event-IDแล้วได้สิ่งที่พลาดไป (EventSource ทำให้เอง)event:คือcreate,update,delete,publish,unpublishหรือleaveโดยdeleteและleaveมีแค่collectionกับidreadyมาเมื่อ stream พร้อม หลังสิ่งที่ client ที่เชื่อมต่อใหม่พลาดไปresetมาเมื่อต่อจากเดิมไม่ได้: หายไปนานกว่าretentionHoursหรือตามหลังเกิน 1,000 event ให้โหลดสิ่งที่แสดงอยู่ใหม่- บรรทัด comment ทุก
heartbeatMsกันไม่ให้ proxy ปิด stream ที่เงียบ
การยืนยันตัวตนเหมือน REST: session cookie ในเบราว์เซอร์ หรือ API key ใน header Authorization
บนเซิร์ฟเวอร์ ไม่รับ token จาก URL ผู้เยี่ยมชม subscribe ได้เฉพาะสิ่งที่อ่านได้ สิทธิ์ตรวจตอนเปิด stream
(where บน field ที่ผู้ใช้อ่านไม่ได้ได้ 403 เหมือนรายการ) stream ที่ session หมดหรือ API key
ถูกยกเลิกจะปิดภายในหนึ่งนาที แล้ว client เชื่อมต่อใหม่ในฐานะผู้ใช้ปัจจุบัน
leave และ delete ส่งเฉพาะผู้ subscribe ที่เห็นเอกสารก่อนการเปลี่ยน: สถานะที่เก็บก่อนหน้าตรงกับ subscription
(where, id) และสิทธิ์อ่านของผู้ subscribe ตอนนี้ (where ระดับแถว, ฉบับร่างตามที่อ่านได้) จึงไม่มีใครรู้ id
ของเอกสารที่ไม่เคยอ่านได้ การตรวจนี้ทำในหน่วยความจำ ถ้าบอกไม่ได้ (like, ค่าใน array) จะไม่ส่งอะไร
client อาจค้างเอกสารนั้นไว้จนโหลดใหม่
Local API
cms.subscribe() ให้ event เดียวกันในโค้ดฝั่งเซิร์ฟเวอร์ ไม่ว่าจะบันทึกจาก instance นี้หรือ instance อื่น
ที่ใช้ฐานข้อมูลเดียวกัน:
const changes = cms.subscribe('posts', { where: { status: { equals: 'published' } } })
await changes.ready
for await (const change of changes) {
if (change.event === 'reset') continue
console.log(change.event, change.id)
}เหมือน Local API อื่นคือเชื่อผู้เรียก ส่ง user และ overrideAccess: false เพื่ออ่านในฐานะผู้ใช้
after ต่อจาก eventId ของ event หนึ่ง close() (หรือ break) เพื่อหยุด
ตัวเลือก
export default defineConfig({
realtime: {
retentionHours: 24, // เก็บ event ไว้ให้ client ที่เชื่อมต่อใหม่
pollMs: 1000, // แต่ละเซิร์ฟเวอร์อ่าน event ใหม่บ่อยแค่ไหน
heartbeatMs: 25_000,
maxPerUser: 10, // stream ต่อผู้ใช้ API key หรือ IP ของผู้เยี่ยมชม ต่อเซิร์ฟเวอร์: เกินได้ 429
maxConnections: 1000, // stream ต่อเซิร์ฟเวอร์: เกินได้ 503
},
// …
})หลาย instance และ serverless
- แต่ละเซิร์ฟเวอร์อ่าน event ใหม่จากฐานข้อมูลทุก
pollMs(ครั้งเดียวสำหรับผู้ subscribe ทั้งหมดของมัน) แล้วส่งให้ stream ของตัวเอง การบันทึกบน instance หนึ่งจึงถึงผู้ subscribe ทุก instance ภายในราวpollMsไม่ใช้ Redis หรือ LISTEN/NOTIFY: instance ใช้ร่วมกันแค่ฐานข้อมูล เหมือน job - Cloudflare Workers เขียนลง stream ของ request หนึ่งจากอีก request ไม่ได้ แต่ละ stream จึงอ่าน
ตารางเอง: หนึ่ง query ต่อ stream ทุก
pollMsบน Workers ควรตั้งpollMsสูงขึ้นหรือมี stream ไม่มาก ขีดจำกัดนับต่อ isolate - ฟังก์ชัน serverless (Vercel, Netlify) ตัด stream เมื่อถึงเวลาสูงสุดของฟังก์ชัน client เชื่อมต่อใหม่
ด้วย
Last-Event-IDและไม่พลาดอะไร แต่ stream ที่เปิดนานกินเวลาฟังก์ชัน ถ้ามีผู้ subscribe มาก เซิร์ฟเวอร์ (easy-cms serve, Docker) เหมาะกว่า - แถวที่เก่ากว่า
retentionHoursถูกลบโดย job ที่รันทุกนาที (หรือ cron ของคุณที่เรียกGET <api>/jobs/run)
ข้อควรรู้
- adapter ฐานข้อมูลที่เขียน outbox ได้ (
@easy-cms/db-sqlite,@easy-cms/db-postgres) เขียน event ใน transaction ของการบันทึก: ไม่มี event ที่ไม่มีการบันทึก และไม่มีการบันทึกที่ไม่มี event adapter อื่นเขียนทันทีหลังบันทึก และเซิร์ฟเวอร์แจ้งตอนเริ่ม - แต่ละ event อ่านเอกสารหนึ่งครั้งต่อผู้ subscribe (ผู้เยี่ยมชมที่ subscribe แบบเดียวกันใช้การอ่านร่วมกัน) ผู้ subscribe จำนวนมากบน collection ที่แก้บ่อยจึงมีค่าอ่าน
- global และ channel ที่กำหนดเอง ยังไม่อยู่ใน realtime
ในหน้า admin
collection ที่มี realtime: true อัปเดตสดในหน้า admin ด้วย collection อื่นทำงานเหมือนเดิมและไม่เปิด stream
- รายการ: จุด "สด" ข้างหัวข้อ (สีเทาพร้อม "กำลังเชื่อมต่อใหม่…" ระหว่าง stream หลุด) แถวที่อยู่บนจอเปลี่ยนในที่ทันทีที่มีคนบันทึก แถวที่ถูกลบจากที่อื่นจางลงพร้อมป้าย "ลบแล้ว" เอกสารใหม่และเอกสารที่ออกจากสิ่งที่รายการแสดงไม่ขยับแถวใต้เคอร์เซอร์ แต่แสดง "มีการเปลี่ยนแปลง 3 รายการ — แสดง" กดแล้วโหลดรายการใหม่ด้วยหน้า การเรียง ตัวกรอง และแถวที่เลือกเดิม
- หน้าแก้ไขและ drawer: การบันทึกอื่นของเอกสารโหลดเองเมื่อไม่มีสิ่งที่ยังไม่บันทึก ("อัปเดตจากการบันทึกล่าสุดแล้ว") ถ้ามี จะแสดง "เอกสารนี้ถูกบันทึกจากที่อื่น" ให้เลือก โหลดฉบับใหม่ (ทิ้งของฉัน) หรือ ใช้ของฉัน หลังจากนั้นการบันทึกจะถามก่อนทับ การบันทึกจากหน้าเดียวกันไม่นับ และไม่บอกว่าใครบันทึก อาจเป็นคนอื่นหรือตัวคุณเองในแท็บอื่น
- ถูกลบ หรือคุณไม่มีสิทธิ์ดูแล้ว: แถบบอก และปุ่มบันทึกถูกปิด สิ่งที่พิมพ์ไว้ยังอยู่ให้คัดลอก
- แต่ละแท็บเปิด stream เดียวสำหรับทั้งหมด ขีดจำกัด 10 ต่อผู้ใช้ (
maxPerUser) จึงประมาณสิบแท็บของ admin ต่อเซิร์ฟเวอร์ การบันทึกสองที่ในเวลาเดียวกันพอดียังทับกันได้ เพราะเซิร์ฟเวอร์ยังไม่ปฏิเสธการบันทึกที่ล้าสมัย
ขั้นต่อไป
- การควบคุมสิทธิ์: ผู้ subscribe แต่ละคนอ่านอะไรได้
- Webhooks: ส่งการเปลี่ยนแปลงไปบริการอื่นแทน