Chương 12: Real-time — Team Làm Việc Cùng Lúc

"Đức drag task sang 'Done'. Trong cùng lúc đó, Tuấn cũng drag task đó sang 'Review'. Board hiện 2 trạng thái khác nhau. 'Race condition!' — Hà la lên từ bàn QA."
🎯 Mục tiêu
- Supabase Realtime subscriptions
- Live board updates (no refresh needed)
- Presence (who's online)
- 🐛 Bug #8: Race condition in task assignment
Phần 1: Supabase Realtime (25 phút)
Enable Realtime
sql
-- Enable realtime for tasks table
ALTER PUBLICATION supabase_realtime ADD TABLE tasks;
ALTER PUBLICATION supabase_realtime ADD TABLE comments;Subscribe to Changes
javascript
// public/static/js/realtime.js
import { createClient } from 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Listen for task changes
const channel = supabase
.channel('board-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'tasks' },
(payload) => {
switch (payload.eventType) {
case 'INSERT':
addTaskToBoard(payload.new)
break
case 'UPDATE':
updateTaskOnBoard(payload.new)
break
case 'DELETE':
removeTaskFromBoard(payload.old)
break
}
}
)
.subscribe()Phần 2: Presence — Who's Online (20 phút)
javascript
// Track who's viewing the board
const presenceChannel = supabase.channel('board-presence')
presenceChannel
.on('presence', { event: 'sync' }, () => {
const state = presenceChannel.presenceState()
renderOnlineUsers(Object.values(state).flat())
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await presenceChannel.track({
user_id: currentUser.id,
name: currentUser.name,
online_at: new Date().toISOString()
})
}
})
function renderOnlineUsers(users) {
const container = document.getElementById('online-users')
container.innerHTML = users.map(u =>
`<div class="avatar online" title="${u.name}">${u.name[0]}</div>`
).join('')
}Phần 3: 🐛 Bug #8 — Race Condition (30 phút)
The Problem
Timeline:
T1: Đức reads task (status: 'todo', assignee: null)
T2: Tuấn reads task (status: 'todo', assignee: null)
T3: Đức assigns to himself → success
T4: Tuấn assigns to himself → ALSO success!
→ Task assigned to BOTH → data corruptionTest
typescript
it('should prevent race condition in task assignment', async () => {
// Simulate concurrent assignment
const [res1, res2] = await Promise.all([
request(app).put(`/api/tasks/${taskId}`)
.set('Authorization', `Bearer ${ducToken}`)
.send({ assignee_id: ducId }),
request(app).put(`/api/tasks/${taskId}`)
.set('Authorization', `Bearer ${tuanToken}`)
.send({ assignee_id: tuanId })
])
// One should succeed, one should fail
const statuses = [res1.status, res2.status].sort()
expect(statuses).toContain(200)
expect(statuses).toContain(409) // Conflict
})Fix: Optimistic Locking
typescript
// Add version column
// ALTER TABLE tasks ADD COLUMN version INTEGER DEFAULT 1;
router.put('/:id', async (req, res) => {
const { id } = req.params
const { version, ...updates } = req.body
const { data, error } = await supabase
.from('tasks')
.update({ ...updates, version: version + 1 })
.eq('id', id)
.eq('version', version) // Only update if version matches
.select()
.single()
if (!data) {
return res.status(409).json({
error: 'Task was modified by another user. Please refresh.'
})
}
res.json(data)
})Phần 4: Lab — Real-time Board (15 phút)
bash
antigravity "Integrate Supabase Realtime into TeamFlow Kanban board:
1. Subscribe to tasks table changes
2. Auto-update cards when another user moves tasks
3. Show online users with green dot
4. Show 'typing...' indicator in comments
5. Handle reconnection gracefully"Homework
- [ ] Real-time board working (2 browsers)
- [ ] Presence showing online users
- [ ] Race condition fixed
- [ ] Reconnection handling
Chương tiếp: Sprint Planning & Analytics →