Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.laportenard.com/llms.txt

Use this file to discover all available pages before exploring further.

Table locks enforce exclusive access at the table level. Before a terminal can open or edit a table’s order, it must hold the lock for that table. Table locks operate independently from order leases. A lease governs write access to a specific order (snapshot level); a table lock governs who may navigate to a table at all (floor plan level). In practice, a terminal acquires the table lock first (on the floor plan), then acquires the order lease when editing.

How it works

Acquiring a lock

  1. Terminal taps a table on the floor plan
  2. Frontend sends TABLE_LOCK_ACQUIRE { tableId } via WebSocket
  3. Hub checks if the table is already locked:
    • Not locked — Grant. Returns TABLE_LOCK_GRANTED { tableId }
    • Locked by same terminal — Grant (re-entrant, extends TTL)
    • Locked by another terminal — Deny. Returns TABLE_LOCK_DENIED { tableId, lockedBy }
    • Locked but expired — Clean up expired lock, then grant
  4. On grant, hub broadcasts TABLE_LOCKS_STATE to all other terminals

Maintaining a lock

  • Locks expire after 45 seconds (TABLE_LOCK_TTL_MS) without renewal
  • Terminals send TABLE_LOCK_HEARTBEAT every 15 seconds
  • Each heartbeat resets the expiry to now + 45s
  • If a terminal crashes or loses connection, the lock auto-expires after the grace period

Releasing a lock

  • Terminal sends TABLE_LOCK_RELEASE { tableId } when navigating away
  • Hub broadcasts TABLE_LOCK_RELEASED { tableId } to all terminals
  • The TableLockGuard component releases the lock automatically on unmount

Lock expiry (tick)

The hub runs a periodic tick every 15 seconds (matching the heartbeat interval):
  1. Query all locks where expires_at <= now
  2. For each expired lock:
    • Send TABLE_LOCK_REVOKED { tableId, reason: "Lock expired" } to the holding terminal
    • Delete the lock from storage

Disconnect grace period

When a terminal’s WebSocket disconnects:
  1. Hub shortens lock expiry to now + 10s (grace period)
  2. If the terminal reconnects within 10 seconds, it can re-acquire
  3. If grace period expires, the normal tick process revokes the lock
  Terminal                              Hub
     │                                   │
     ├──── WS Disconnect ───────────────►│  (unexpected close)
     │                                   │
     │                                   │  lock.expires_at = now + 10s
     │                                   │
     │      (reconnects within 10s)      │
     ├──── WS Connect ──────────────────►│
     │◄─── AUTH_OK ──────────────────────┤
     │◄─── TABLE_LOCKS_STATE ────────────┤  (current lock state)
     │                                   │
     │      (TableLockGuard re-acquires) │
     ├──── TABLE_LOCK_ACQUIRE { id } ───►│
     │◄─── TABLE_LOCK_GRANTED { id } ────┤  resume editing

Manager override (force acquire)

Managers can forcibly take over a table locked by another terminal:
1

Request

Manager terminal sends TABLE_LOCK_FORCE_ACQUIRE { tableId }
2

Validate

Hub validates that the requesting terminal has manager or group_pos_manager role (server-side check)
3

Revoke current holder

Hub sends TABLE_LOCK_REVOKED { tableId, reason: "Manager override" } to the current holder
4

Grant to manager

Manager receives TABLE_LOCK_GRANTED { tableId }. Hub broadcasts updated TABLE_LOCKS_STATE
The handler is the single authority for revoke notifications during force-acquire. The onRevoke callback is only for async events (TTL tick expiry) — never both, to prevent duplicate notifications.
Non-managers see a “Table Locked” dialog with only a “Dismiss” button. Managers see an additional “Take Over” button.

Protocol messages

Terminal to hub (4 message types)

TypePurposeKey fields
TABLE_LOCK_ACQUIRERequest table locktableId
TABLE_LOCK_RELEASERelease table locktableId
TABLE_LOCK_HEARTBEATKeep lock alivetableId
TABLE_LOCK_FORCE_ACQUIREManager overridetableId

Hub to terminal (5 message types)

TypePurposeKey fields
TABLE_LOCK_GRANTEDLock acquiredtableId
TABLE_LOCK_DENIEDLock rejectedtableId, lockedBy: { userId, userName }
TABLE_LOCK_RELEASEDLock released by holdertableId
TABLE_LOCK_REVOKEDLock taken awaytableId, reason
TABLE_LOCKS_STATEFull lock snapshotlocks: [{ tableId, terminalId, userId, userName }]

Frontend lock handling

Hub transport

  • heldTableLocks: Set<number> — Table IDs currently locked by this terminal
  • pendingTableLocks: Map<number, { resolve, timer }> — In-flight acquire requests
  • Separate heartbeat timer: sends TABLE_LOCK_HEARTBEAT for all held locks every 15s
acquireTableLock() and forceAcquireTableLock() return a Promise<TableLockResult> that resolves on TABLE_LOCK_GRANTED or TABLE_LOCK_DENIED, or returns { granted: false } on timeout (fail-closed).

Direct transport

In direct mode, table locking is disabled. All acquireTableLock() calls return { granted: true } immediately.

TableLockGuard

Renderless component on the order page (realtime/TableLockGuard.tsx):
ResponsibilityHow
Release on unmountuseEffect cleanup calls releaseTableLock(tableId)
Re-acquire on reconnectonStateChange("connected") triggers acquireTableLock(tableId)
Redirect on revocationonTableLockRevoked(tableId) calls router.replace("/")

FloorPlanScreen

  • Double-tap guardpendingLockTableRef cancels stale in-flight acquires on new table tap
  • Acquire before navigate — Lock acquired, then navigate to order page on success
  • TableLockedDialog — Shown on deny with the holder’s name. Managers see “Take Over”
  • Cancel cleanup — Dismissing modals releases any pending lock
  • Transfer mode — Destination table lock acquired before transfer navigation
  • Lock state subscription — Locked tables show amber label text on the floor plan

Table locks vs order leases

AspectTable lockOrder lease
ScopeTable (floor plan level)Order (snapshot level)
When acquiredTapping a table on the floor planOpening an order for editing
TTL45s60s
Heartbeat15s20s
Force acquireRequires manager role (server-enforced)force: true flag in LEASE_ACQUIRE
Fail-closed timeoutYes — deny on timeoutNo — deny on timeout
Offline behaviorDisabled (returns granted: true)Denied (returns granted: false)

Timing summary

ParameterValueDefined in
Lock TTL45sTABLE_LOCK_TTL_MS in constants.ts
Heartbeat interval15sTABLE_LOCK_HEARTBEAT_MS in constants.ts
Disconnect grace10sGRACE_MS in constants.ts (shared with leases)
Acquire timeout (frontend)10sLEASE_TIMEOUT_MS in hubTransport.ts
Tick interval (hub)15sTABLE_LOCK_HEARTBEAT_MS in index.ts