Web Locks
ทำไมต้องใช้ lock?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ lock?”หลายแท็บของแอปเดียวกันสามารถทำงานพร้อมกันและอาจพยายามเขียนลง IndexedDB record หรือไฟล์ OPFS เดียวกันในเวลาเดียวกัน หากไม่มีการประสานงาน การเขียนจะ race กันและ writer สุดท้ายจะชนะโดยไม่มีการแจ้งเตือน (หรือข้อมูลเสียหาย) Web Locks API มี primitive สำหรับ mutual-exclusion ใน browser โดยไม่ต้องใช้ server
navigator.locks.request(name, callback)
หัวข้อที่มีชื่อว่า “navigator.locks.request(name, callback)”ในการขอ lock ส่งชื่อ string และ async callback browser จะถือ lock ตราบเท่าที่ Promise ที่ callback คืนกลับยัง pending แล้วจึงปล่อยโดยอัตโนมัติ
await navigator.locks.request('demo:my-resource', async (lock) => { // Inside here you hold the lock exclusively. // No other tab can acquire 'demo:my-resource' until this async block resolves. await doWork();});// Lock released here — next waiter can proceed.หาก lock ถูกถือโดยแท็บอื่นอยู่ request จะ queue callback ของคุณจนกว่า lock จะถูกปล่อย คิวเป็นแบบ FIFO
โหมด exclusive กับ shared
หัวข้อที่มีชื่อว่า “โหมด exclusive กับ shared”โหมดเริ่มต้นคือ exclusive — มีผู้ถือได้ทีละคนเท่านั้น สำหรับ workload ที่อ่านหนักใช้โหมด shared ได้: ผู้ถือ shared หลายคนสามารถทับซ้อนกันได้ แต่คำขอ exclusive จะรอจนผู้ถือ shared ทั้งหมดเสร็จ (เหมือน read-write lock)
// Multiple tabs can hold this concurrentlyawait navigator.locks.request('demo:db-read', { mode: 'shared' }, async (lock) => { const data = await db.getAll(); return data;});
// This waits until every shared holder above has finishedawait navigator.locks.request('demo:db-read', { mode: 'exclusive' }, async (lock) => { await db.put({ key: 'x', value: 42 });});ifAvailable — ลอง non-blocking
หัวข้อที่มีชื่อว่า “ifAvailable — ลอง non-blocking”ส่ง ifAvailable: true เพื่อรับ null แทนการ queue เมื่อ lock ถูกถืออยู่ มีประโยชน์สำหรับ “ทำงานเฉพาะเมื่อเริ่มได้ทันที มิฉะนั้นข้ามรอบนี้”:
const result = await navigator.locks.request( 'demo:background-sync', { ifAvailable: true }, async (lock) => { if (!lock) { console.log('Lock busy — skipping this cycle'); return null; } // We have the lock return await syncData(); });navigator.locks.query()
หัวข้อที่มีชื่อว่า “navigator.locks.query()”query() คืน snapshot ของ lock ที่ถือและรออยู่ทั้งหมด — มีประโยชน์สำหรับ debug:
const state = await navigator.locks.query();console.log('Held:', state.held);console.log('Pending:', state.pending);รันได้: ขอ exclusive lock
หัวข้อที่มีชื่อว่า “รันได้: ขอ exclusive lock”ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
Web Locks โหมด exclusive | ป้องกัน race condition ข้ามแท็บได้แน่นอน เพราะ browser จัดคิว FIFO ให้เอง | ถ้า callback ค้างไม่ resolve/reject lock จะไม่ถูกปล่อย ทำให้แท็บอื่น deadlock รอตลอดไป |
Web Locks โหมด shared | อ่านพร้อมกันได้หลายแท็บ ไม่ block workload ที่อ่านหนัก | ต้องผสมกับ exclusive ให้ถูกจังหวะ ไม่งั้นเสี่ยง read ข้อมูลระหว่างที่ writer กำลังแก้ไขอยู่ |
ifAvailable: true (non-blocking) | ไม่ต้องรอคิว เหมาะกับงาน background ที่ข้ามรอบได้โดยไม่กระทบผู้ใช้ | ถ้าใช้ผิดที่กับงานที่ต้องรันแน่นอน อาจทำให้งานสำคัญถูกข้ามไปเงียบๆ |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ขอ lock โดยไม่มีกลยุทธ์ timeout/abort — ถ้าแท็บที่ถือ lock ค้างหรือ crash กลางทาง เช่น network hang ใน callback แท็บอื่นจะรอ lock นั้นตลอดไปโดยไม่มี error ใดๆ ควรส่ง
AbortSignalผ่าน optionsignalเพื่อกำหนด timeout - คิดว่าต้องเรียก method “release” เองเหมือน mutex ในภาษาอื่น — Web Locks ผูกกับ lifetime ของ Promise ที่ callback คืนกลับ lock จะถูกปล่อยอัตโนมัติเมื่อ Promise settle เท่านั้น ไม่มี method ให้เรียกตรงๆ
- ใช้
exclusivemode สำหรับงานอ่านอย่างเดียว — ทำให้ทุก read ต้อง serialize โดยไม่จำเป็น ทั้งที่ควรใช้sharedmode เพื่อให้อ่านพร้อมกันได้
💡 ตัวอย่างจากของจริง
Google Docs — ใช้ pattern คล้าย Web Locks สำหรับ leader election ให้แท็บเดียวรับผิดชอบ sync กับ server ป้องกันการเขียนซ้อนกันจากหลายแท็บ
Photoshop (web version) — ใช้ lock-like coordination ป้องกันหลายแท็บเขียนไฟล์ OPFS เดียวกันพร้อมกันจนข้อมูลเสียหาย