الوقت الفعلي والقنوات في SDK جديد
تتيح لك وحدة الوقت الفعلي الاشتراك في تغييرات قاعدة البيانات وبث الرسائل بين العملاء وتتبع من المتصل وبناء أنظمة دردشة كاملة بقنوات دائمة — كلها عبر اتصال WebSocket واحد يديره SDK.
لا إعداد مطلوباً في لوحة التحكم
الوقت الفعلي على مستوى SDK/المحرك — لا إعداد منفصل في لوحة التحكم. تُنشأ القنوات عند الطلب عبر SDK (أو REST API على /api/v0/channels)، وendpoints القنوات نطاق مستخدم، لذا تتطلب جلسة مصادَقة.
تغييرات قاعدة البيانات
احصل على إشعار فوري عند إدراج صفوف أو تحديثها أو حذفها:
const channel = dypai.realtime.channel('orders-updates')
channel
.on('postgres_changes', { event: '*', table: 'orders' }, (payload) => {
console.log('Event:', payload.eventType) // INSERT, UPDATE, or DELETE
console.log('New data:', payload.new)
console.log('Old data:', payload.old)
})
.subscribe()
التصفية حسب نوع الحدث
// Only new orders
channel.on('postgres_changes', { event: 'INSERT', table: 'orders' }, callback)
// Only status changes
channel.on('postgres_changes', {
event: 'UPDATE',
table: 'orders',
filter: 'status=eq.completed'
}, callback)
البث
أرسل رسائل عابرة بين العملاء المتصلين — دون قاعدة بيانات:
const channel = dypai.realtime.channel('room-1')
// Listen
channel.on('broadcast', { event: 'typing' }, (payload) => {
console.log(`${payload.user} is typing...`)
})
channel.subscribe()
// Send
channel.send('typing', { user: 'John' })
الحضور
تتبع من المتصل في الوقت الفعلي:
const channel = dypai.realtime.channel('room-1')
channel.on('presence', { event: 'sync' }, () => {
const users = channel.presenceState()
console.log('Online:', Object.keys(users))
})
channel.subscribe()
// Announce yourself
channel.track({ user_id: '123', name: 'John' })
خطافات React
useRealtime — تغييرات قاعدة البيانات
import { useRealtime } from '@dypai-ai/client-sdk/react'
function OrderList() {
const { data: orders, refetch } = useEndpoint('list_orders')
useRealtime('orders', {
onInsert: () => refetch(),
onUpdate: () => refetch(),
onDelete: () => refetch(),
})
return <ul>{orders?.map(o => <li key={o.id}>{o.status}</li>)}</ul>
}
| Parameter | Type | Description |
|---|---|---|
tablerequired | string | اسم الجدول للاشتراك فيه |
event | '*' | 'INSERT' | 'UPDATE' | 'DELETE'= '*' | تصفية نوع الحدث |
filter | object | التصفية بقيم الحقول، مثلاً { status: 'active' } |
onInsert(record) | function | يُستدعى عند إدراج صف |
onUpdate(record, old) | function | يُستدعى عند تحديث صف |
onDelete(old) | function | يُستدعى عند حذف صف |
onChange(change) | function | يُستدعى عند أي تغيير |
enabled | boolean= true | تفعيل/تعطيل الاشتراك |
يُرجع: changes و isConnected و status و error
useChannel — البث والحضور
import { useChannel } from '@dypai-ai/client-sdk/react'
function Editor() {
const { presences, broadcast, memberCount } = useChannel('doc:abc', {
presence: { name: 'Alice' },
onBroadcast: (msg) => {
if (msg.event === 'cursor') updateCursor(msg.sender, msg.payload)
},
})
return <span>{memberCount} online</span>
}
| Parameter | Type | Description |
|---|---|---|
channelrequired | string | اسم القناة (أي سلسلة) |
presence | boolean | object | تتبع الحضور. مرّر كائناً ببيانات مخصصة |
onBroadcast(msg) | function | يُستدعى عند رسائل البث |
onJoin(userId, data) | function | يُستدعى عندما ينضم أحد |
onLeave(userId, data) | function | يُستدعى عندما يغادر أحد |
enabled | boolean= true | تفعيل/تعطيل |
يُرجع: broadcast(event, payload?) و presences و isConnected و memberCount
useChannelMessages — دردشة دائمة
مراسلة كاملة مع تخزين في قاعدة البيانات وتسليم في الوقت الفعلي:
import { useChannelMessages } from '@dypai-ai/client-sdk/react'
function TeamChat({ channelId }) {
const { messages, send, isLoading, isConnected } = useChannelMessages(channelId)
return (
<div>
{messages.map(m => (
<div key={m.id}>
<b>{m.sender_email}</b>: {m.content}
</div>
))}
<input onKeyDown={e => {
if (e.key === 'Enter') { send(e.target.value); e.target.value = '' }
}} />
</div>
)
}
| Parameter | Type | Description |
|---|---|---|
channelIdrequired | string | معرّف القناة (مثلاً 'ch_abc123') |
loadHistory | boolean= true | تحميل سجل الرسائل عند التركيب |
limit | number= 50 | الحد الأقصى للرسائل المراد تحميلها |
يُرجع: messages و send(content, metadata?) و isLoading و isConnected و error
useChannels — إدارة القنوات
import { useChannels } from '@dypai-ai/client-sdk/react'
function ChannelList() {
const { channels, createChannel } = useChannels()
return (
<div>
{channels.map(ch => <div key={ch.id}>{ch.name} ({ch.member_count})</div>)}
<button onClick={() => createChannel('General', [], 'public')}>New</button>
</div>
)
}
| Parameter | Type | Description |
|---|---|---|
channels | Channel[] | قنوات المستخدم + القنوات العامة |
createChannel(name, members?, type?) | async | إنشاء قناة. النوع: 'group' أو 'dm' أو 'public' |
isLoading | boolean | حالة التحميل |
refetch() | async | إعادة تحميل قائمة القنوات |
أنواع القنوات
| النوع | الوصف | من يمكنه الانضمام | إزالة التكرار التلقائي |
|---|---|---|---|
group | دردشة مجموعة خاصة | الأعضاء المدعوون فقط | لا |
dm | رسالة مباشرة (واحد لواحد) | المشاركان | نعم — لن يُنشئ تكرارات |
public | قناة مفتوحة | أي شخص (ينضم تلقائياً عند الرسالة) | لا |
// DM — calling twice with same users returns the existing channel
const dm = await createChannel('Alice ↔ Bob', ['bob-user-id'], 'dm')
أدوار الأعضاء
| الإجراء | admin | member |
|---|---|---|
| إرسال/قراءة الرسائل | ✅ | ✅ |
| تعديل/حذف رسائله | ✅ | ✅ |
| حذف رسائل الآخرين | ✅ | ❌ |
| إضافة/إزالة الأعضاء | ✅ | ❌ |
| تغيير الأدوار | ✅ | ❌ |
يحصل منشئ القناة على دور admin تلقائياً.
REST API للقنوات
endpoints مضمّنة على /api/v0/channels — لا إعداد سير عمل مطلوباً:
| الطريقة | المسار | الوصف |
|---|---|---|
POST | /channels | إنشاء قناة |
GET | /channels | سرد القنوات |
GET | /channels/:id | تفاصيل القناة + الأعضاء |
PATCH | /channels/:id | تحديث الاسم |
DELETE | /channels/:id | حذف القناة |
GET | /channels/users | سرد مستخدمي المشروع |
POST | /channels/:id/messages | إرسال رسالة |
GET | /channels/:id/messages | السجل (?limit=50&before=timestamp) |
PATCH | /channels/:id/messages/:msgId | تعديل رسالة |
DELETE | /channels/:id/messages/:msgId | حذف ناعم |
GET | /channels/:id/unread | عدد غير المقروء |
POST | /channels/:id/read | وضع علامة مقروء |
POST | /channels/:id/join | الانضمام إلى قناة عامة |
POST | /channels/:id/leave | مغادرة القناة |
POST | /channels/:id/members | إضافة أعضاء |
DELETE | /channels/:id/members/:userId | إزالة عضو |
PATCH | /channels/:id/members/:userId | تغيير الدور |
GET | /channels/:id/members | سرد الأعضاء |
سلوك الاتصال
- WebSocket واحد — تتشارك كل الخطافات اتصالاً واحداً. لا تكرارات.
- إعادة اتصال تلقائية — تراجع أسي (1s → 2s → 5s → 10s → 30s).
- نبض القلب — نبض كل 25s لإبقاء الاتصال حياً.
- إعادة تشغيل تلقائية — تُعاد الاشتراكات وانضمام القنوات عند إعادة الاتصال.
- تبويبات متعددة — لكل تبويب اتصاله. يتتبع الحضور كلاً على حدة.
مثال كامل — دردشة الفريق
import {
DypaiProvider, useAuth, useChannelMessages, useChannels, useChannel
} from '@dypai-ai/client-sdk/react'
import { dypai } from './lib/dypai'
function App() {
return (
<DypaiProvider client={dypai}>
<Chat />
</DypaiProvider>
)
}
function Chat() {
const { user } = useAuth()
const myName = user?.email?.split('@')[0] || 'anonymous'
const { channels, createChannel } = useChannels()
const [activeId, setActiveId] = useState(channels[0]?.id)
return (
<div className="flex">
<aside className="w-56">
{channels.map(ch => (
<button key={ch.id} onClick={() => setActiveId(ch.id)}>
# {ch.name}
</button>
))}
<button onClick={() => createChannel('New Channel')}>+</button>
</aside>
{activeId && <ChatRoom channelId={activeId} myName={myName} />}
</div>
)
}
function ChatRoom({ channelId, myName }) {
const { messages, send, isLoading, isConnected } = useChannelMessages(channelId)
const { presences } = useChannel(`channel:${channelId}`, {
presence: { name: myName },
})
// Typing indicator
const [typingUsers, setTypingUsers] = useState(new Set())
const { broadcast } = useChannel(`channel:${channelId}:typing`, {
onBroadcast: (msg) => {
if (msg.event === 'typing' && msg.payload.name !== myName) {
setTypingUsers(prev => new Set(prev).add(msg.payload.name))
setTimeout(() => setTypingUsers(prev => {
const next = new Set(prev); next.delete(msg.payload.name); return next
}), 2000)
}
},
})
const [input, setInput] = useState('')
return (
<div className="flex-1 flex flex-col">
<div className="p-2 border-b flex justify-between">
<span>{isConnected ? '● Live' : '○ Offline'}</span>
<span>{Object.values(presences).map(p => p.data?.name).join(', ')}</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoading && <p>Loading...</p>}
{messages.map(m => (
<div key={m.id}>
<b>{m.sender_email}</b>: {m.content}
</div>
))}
{typingUsers.size > 0 && (
<p className="text-sm italic">{[...typingUsers].join(', ')} typing...</p>
)}
</div>
<div className="p-3 border-t">
<input
value={input}
onChange={e => { setInput(e.target.value); broadcast('typing', { name: myName }) }}
onKeyDown={e => { if (e.key === 'Enter') { send(input); setInput('') } }}
placeholder="Type a message..."
className="w-full border rounded px-3 py-2"
/>
</div>
</div>
)
}
التنظيف
// Unsubscribe from a specific channel
channel.unsubscribe()
// Remove all subscriptions
dypai.realtime.removeAllChannels()