Services & Adapters

LockService

Service per lock pessimistico via RPC PostgREST. acquisisciLock, rilasciaLock, rinnovaLock e leggiStatoLock.

Costruttore

class LockService {
  constructor(private readonly baseUrl: string)
}

Service per lock pessimistico sui fogli. Quattro operazioni, tre RPC e una read.

API

acquisisciLock(idfolio)

async acquisisciLock(idfolio: number): Promise<AcquisisciLockResult>

RPC POST /rpc/acquisisci_lock_foglio con { p_idfolio }. Acquisisce un lock di 30 minuti (durata gestita server-side).

interface AcquisisciLockResult {
  successo: boolean
  idutente_lock?: number          // se successo === true
  datalock?: string               // ISO 8601
  scadenzalock?: string           // ISO 8601, ~30 min nel futuro
  errore?: 'LOCK_OCCUPIED'        // se successo === false
}

Comportamento:

  • Se nessun lock attivo → crea il lock per current_user, ritorna successo: true
  • Se lock esistente di altro utente → ritorna { successo: false, errore: 'LOCK_OCCUPIED' }
  • Se lock esistente dello stesso utente → estende la scadenza (idempotente)
  • Se lock scaduto (scadenzalock < now()) → sovrascrive silenziosamente

rilasciaLock(idfolio)

async rilasciaLock(idfolio: number): Promise<boolean>

RPC POST /rpc/rilascia_lock_foglio con { p_idfolio }. Elimina la riga lock se l'utente corrente è owner.

Side-effects:

  • Se current_user === idutente_lock → DELETE lock
  • Altrimenti → no-op (silent fail per evitare error leak)

Ritorna sempre true se la chiamata HTTP è riuscita.

rinnovaLock(idfolio)

async rinnovaLock(idfolio: number): Promise<string>

RPC POST /rpc/rinnova_lock_foglio con { p_idfolio }. Estende scadenzalock a now() + 30 min. Usato come heartbeat ogni 5 minuti dal composable useSpreadsheet.

Ritorna la nuova scadenzalock (ISO 8601 string).

Lato server, accetta solo se current_user === idutente_lock.

leggiStatoLock(idfolio)

async leggiStatoLock(idfolio: number): Promise<LockInfo>

GET sulla vista v_foglidicalcolo_lista filtrata su idfolio=eq.{id}. Ritorna lo stato corrente lock mappato in camelCase:

interface LockInfo {
  idfolio: number
  idutenteLock: number | null     // ← mappato da idutente_lock
  datalock: string | null
  scadenzalock: string | null
  islocked: boolean               // true se lock attivo non scaduto
}

Usato per polling manuale o per leggere lo stato senza tentare di acquisirlo.

Heartbeat pattern

// In useSpreadsheet
let heartbeatInterval: ReturnType<typeof setInterval> | null = null

async function startHeartbeat(idfolio: number) {
  heartbeatInterval = setInterval(async () => {
    try {
      const nuovaScadenza = await lockService.rinnovaLock(idfolio)
      // aggiorna lockInfo.scadenzalock localmente
    } catch (err) {
      // lock perso (es. invalidato da admin)
      stopHeartbeat()
    }
  }, 5 * 60 * 1000)    // ogni 5 min
}

function stopHeartbeat() {
  if (heartbeatInterval) {
    clearInterval(heartbeatInterval)
    heartbeatInterval = null
  }
}

Logica islocked

Il campo islocked in v_foglidicalcolo_lista è calcolato server-side come:

CASE
  WHEN scadenzalock IS NULL THEN false
  WHEN scadenzalock < now() THEN false
  WHEN idutente_lock = current_user_id() THEN false  -- vediamo "no lock esterno"
  ELSE true
END
Invariante: chi possiede il lock vede islocked === false, chi non lo possiede vede islocked === true. Combinato con idutente_lock non-null, questo permette di distinguere "ho io il lock" (lockInfo !== null && !islocked) da "lock di altri" (islocked === true).

Esempio: acquisizione con fallback

import { LockService } from '@pzeta/vue-foglidicalcolo'

const lockSvc = new LockService('https://api.example.com/rest')

const result = await lockSvc.acquisisciLock(42)

if (result.successo) {
  console.log(`Lock acquisito, scade ${result.scadenzalock}`)
  // ... apri editor in modalità edit
} else if (result.errore === 'LOCK_OCCUPIED') {
  console.warn('Foglio bloccato da altro utente')
  const stato = await lockSvc.leggiStatoLock(42)
  console.log(`Owner: utente ${stato.idutenteLock}, scade ${stato.scadenzalock}`)
  // ... apri editor in modalità view forzata
}

Pulizia lock orfani

I lock vengono auto-puliti dalla logica scadenzalock < now() lato DB. Lock fantasma (es. utente chiuso bruscamente, heartbeat interrotto) scadono entro 30 minuti. Il prossimo acquisisciLock di un altro utente li sovrascrive silenziosamente.

Se serve liberare un lock prima della scadenza (es. admin override), implementa una RPC server-side dedicata (forza_rilascio_lock) — la libreria non la include perché richiede autorizzazione speciale.

Tipi correlati