Skip to content

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 corruption

Test

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 →

Powered by CodyMaster × VitePress