Cross-Tab Shared State
ปัญหา: ทำให้แท็บ sync กัน
หัวข้อที่มีชื่อว่า “ปัญหา: ทำให้แท็บ sync กัน”ลองนึกภาพ dashboard แบบ multi-tab แท็บ A เพิ่ม counter แท็บ B ควรเห็นค่าใหม่โดยไม่ต้อง polling storage event บน localStorage สามารถแจ้งแท็บ B ได้ แต่ส่งเฉพาะค่า string ใหม่ดิบๆ — ไม่มี structured state layer ในตัว
วิธีแก้ปัญหาที่แข็งแกร่งรวม primitive สามตัว:
localStorage(หรือ IndexedDB) — แหล่งความจริงที่คงทน- BroadcastChannel — แจ้งเตือนทันทีไปยังทุกแท็บอื่นเมื่อ state เปลี่ยน
- Web Locks — ป้องกันสองแท็บเขียนพร้อมกัน (leader election หรือ write serialisation)
Pattern: broadcast-on-write
หัวข้อที่มีชื่อว่า “Pattern: broadcast-on-write”Pattern ที่ง่ายที่สุด: ทุกแท็บที่เขียน state จะ broadcast การเปลี่ยนแปลงด้วย
const ch = new BroadcastChannel('demo:app-state');
async function setState(key, value) { await navigator.locks.request('demo:state-write', async () => { localStorage.setItem(key, JSON.stringify(value)); ch.postMessage({ type: 'STATE_UPDATE', key, value }); });}
// React to changes from other tabsch.onmessage = (e) => { if (e.data.type === 'STATE_UPDATE') { console.log('Remote update:', e.data.key, '=', e.data.value); // Update local UI here }};Lock ทำให้แน่ใจว่าหากสองแท็บพยายามเขียนพร้อมกัน จะ queue แทนที่จะ race
Leader election ด้วย Web Locks
หัวข้อที่มีชื่อว่า “Leader election ด้วย Web Locks”สำหรับงานที่ใช้ resource มาก (เช่น แท็บเดียว polling API) ต้องการให้แท็บ “leader” แค่หนึ่งแท็บทำงานและ broadcast ผลลัพธ์ให้ follower Web Locks ทำให้ทำได้ง่าย:
async function runAsLeader() { // Only one tab can hold 'leader' at a time. // The lock is held for the entire async block — when this tab closes the lock // releases and the next queued tab becomes leader. await navigator.locks.request('demo:leader', async () => { console.log('I am the leader'); while (true) { const data = await fetchLatestData(); ch.postMessage({ type: 'DATA_REFRESH', data }); await new Promise((r) => setTimeout(r, 5000)); } });}
runAsLeader();เมื่อ leader tab ปิด browser ปล่อย lock และ callback ของแท็บถัดไปในคิวจะทำงาน — leader handoff อัตโนมัติโดยไม่ต้องเขียน code เพิ่ม
รันได้: shared counter sketch
หัวข้อที่มีชื่อว่า “รันได้: shared counter sketch”ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
รวม 3 primitive (localStorage + BroadcastChannel + Web Locks) | ได้ shared-state layer ที่ทั้ง persistent, real-time และปลอดภัยจาก race condition | ซับซ้อนกว่าการใช้ primitive เดียว ต้องดูแลทั้งสาม concept พร้อมกันและ debug ยากขึ้น |
| broadcast-on-write โดยไม่มี lock | เขียน code ง่าย ไม่ต้องรอ queue | เสี่ยง race condition เมื่อสองแท็บเขียนทับกันแบบ overlapping ในเวลาใกล้กัน |
| leader election ด้วย Web Locks | มีแท็บเดียวทำงานหนัก เช่น polling API ประหยัด resource และลด duplicate request | ถ้า leader tab ค้างโดยไม่ปล่อย lock แท็บอื่นจะไม่มีวันได้เป็น leader |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- พึ่งพา
storageevent เพื่อ sync state ภายในแท็บเดียวกับที่เขียนเอง — event นี้ยิงเฉพาะแท็บอื่นเท่านั้น แท็บที่เขียนต้องอัปเดต UI ของตัวเองด้วย code แยกต่างหาก - ไม่ wrap read-then-write (check-then-act) ไว้ใน lock callback เดียวกัน — ถ้า read อยู่นอก lock race condition ยังเกิดได้แม้จะใช้ Web Locks อยู่แล้ว
- ปล่อยให้ leader tab ทำงานหนักโดยไม่มี fallback เมื่อทุกแท็บถูกปิด — leader election ต้องเริ่มใหม่ทุกครั้งที่เปิดแท็บแรก ไม่มีแท็บใด “จำ” สถานะก่อนหน้าไว้ ต้องพึ่ง
localStorageเป็นตัวกู้คืน state
💡 ตัวอย่างจากของจริง
Google Docs — sync edit state, cursor position และ presence indicator ข้ามแท็บที่เปิดเอกสารเดียวกันด้วย pattern คล้าย broadcast-on-write
Notion — ใช้ leader-election pattern ให้แท็บเดียวรับผิดชอบ sync กับ server แล้ว broadcast ผลลัพธ์ไปยังแท็บอื่น ลด duplicate network request จากหลายแท็บพร้อมกัน