🔨 Máquina de Estados del Remate (FSM)

Sistema de Gestión de Subastas - PujasOnline Streaming Service

← Volver al Inicio

🎯 Visión General

La Máquina de Estados Finita (FSM) del sistema de remate controla el ciclo de vida de cada lote en una subasta. Gestiona los diferentes estados del lote, valida transiciones, controla el temporizador y emite eventos en tiempo real a todos los participantes.

💡 Características Clave
  • Control de estado centralizado (OPEN, GOING_ONCE, GOING_TWICE, SOLD, etc.)
  • Temporizador con soporte para pausar/reanudar
  • Anti-sniping: extensión automática si hay pujas de último momento
  • Validación de pujas (mínimo incremento, postor válido)
  • Sincronización en tiempo real vía DataBus (WebRTC DataChannel)
  • Auditoría completa en MongoDB

📊 Estados del Lote

Un lote puede estar en uno de los siguientes estados:

🟢 OPEN - Abierto
El lote está abierto para recibir pujas. Los postores pueden pujar libremente. El temporizador está corriendo.
🟡 GOING_ONCE - Primera advertencia
El rematador ha anunciado "Going Once". Es una advertencia de que el lote está por cerrarse. Los postores aún pueden pujar.
🟡 GOING_TWICE - Segunda advertencia
El rematador ha anunciado "Going Twice". Es la última advertencia antes de declarar vendido. Los postores aún pueden pujar.
✅ SOLD - Vendido
El lote ha sido adjudicado al mejor postor. No se aceptan más pujas. El lote pasa a estado final.
⏸️ PAUSED - Pausado
El lote está pausado temporalmente. El temporizador se detiene y no se aceptan pujas. Se puede reanudar con el comando RESUME_LOT.
🚫 PASSED - No vendido
El lote no alcanzó el precio mínimo o no recibió pujas. No se vendió. El lote pasa a estado final.
❌ CANCELED - Cancelado
El lote fue cancelado manualmente. No se aceptan más pujas. El lote pasa a estado final.

🎮 Comandos Disponibles

Los siguientes comandos pueden ser ejecutados por el rematador (rol AUCTIONEER) a través de WebSocket:

Comando Descripción Parámetros
OPEN_LOT Abre un nuevo lote para pujas lotId, durationMs, minIncrement (opcional)
PAUSE_LOT Pausa el lote actual (detiene temporizador) lotId
RESUME_LOT Reanuda un lote pausado lotId
GO_ONCE Anuncia "Going Once" (primera advertencia) lotId
GO_TWICE Anuncia "Going Twice" (segunda advertencia) lotId
DECLARE_AWARD Declara vendido al mejor postor lotId
SNAPSHOT_REQUEST Solicita el estado actual del lote lotId
FINALIZE_AUCTION Finaliza el remate completo (graba y archiva) -

Ejemplo de envío de comandos (WebSocket)

// Abrir lote con duración de 30 segundos
ws.send(JSON.stringify({
  kind: 'OPEN_LOT',
  lotId: 'sala-100-LOT',
  payload: { durationMs: 30000 }
}));

// Pausar lote
ws.send(JSON.stringify({
  kind: 'PAUSE_LOT',
  lotId: 'sala-100-LOT'
}));

// Anunciar "Going Once"
ws.send(JSON.stringify({
  kind: 'GO_ONCE',
  lotId: 'sala-100-LOT'
}));

// Declarar vendido
ws.send(JSON.stringify({
  kind: 'DECLARE_AWARD',
  lotId: 'sala-100-LOT'
}));

📡 Eventos de la FSM

La FSM emite eventos en tiempo real que son recibidos por todos los participantes (rematador y postores) vía WebSocket y DataChannel:

Evento Descripción Payload
LOT_OPENED Nuevo lote abierto state, durationMs, timeLeftMs, ownerId
LOT_PAUSED Lote pausado state, timeLeftMs
LOT_RESUMED Lote reanudado state, timeLeftMs
GOING_ONCE Primera advertencia state, timeLeftMs
GOING_TWICE Segunda advertencia state, timeLeftMs
BID_PLACED Nueva puja recibida amount, userId, timeLeftMs, leader
BID_REJECTED Puja rechazada reason (incremento insuficiente, lote cerrado, etc.)
AWARD_DECLARED Lote vendido winnerId, winningAmount, state: 'SOLD'
LOT_PASSED Lote no vendido state: 'PASSED'
LOT_SNAPSHOT Estado actual del lote state, leader, timeLeftMs, minIncrement

Ejemplo de recepción de eventos

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  if (message.type === 'EVENT') {
    console.log('Evento FSM:', message.kind);
    console.log('Payload:', message.payload);

    switch (message.kind) {
      case 'LOT_OPENED':
        console.log('Lote abierto:', message.payload.state);
        break;
      case 'BID_PLACED':
        console.log('Nueva puja:', message.payload.amount);
        console.log('Líder:', message.payload.leader);
        break;
      case 'AWARD_DECLARED':
        console.log('Lote vendido a:', message.payload.winnerId);
        break;
    }
  }
};

🔄 Flujo Típico de un Remate

🎬 INICIO DEL REMATE
📢 Rematador: OPEN_LOT
🟢 Estado: OPEN
💰 Postores: PLACE_BID (múltiples pujas)
⏰ Temporizador activo (con anti-sniping)
📣 Rematador: GO_ONCE
🟡 Estado: GOING_ONCE
📣 Rematador: GO_TWICE
🟡 Estado: GOING_TWICE
🔨 Rematador: DECLARE_AWARD
✅ Estado: SOLD
🏁 FIN DEL LOTE
⚠️ Anti-Sniping

Si se recibe una puja cuando quedan menos de antiSnipingMs milisegundos (por defecto 2000ms), el temporizador se extiende automáticamente para dar tiempo a otros postores de responder.

💻 Ejemplos de Uso

1. Abrir un lote de 60 segundos

// Desde la vista del rematador (host-asm-stream.html)
const lotId = 'sala-100-LOT-001';
const durationMs = 60000; // 60 segundos

ws.send(JSON.stringify({
  kind: 'OPEN_LOT',
  lotId: lotId,
  payload: {
    durationMs: durationMs,
    minIncrement: 100, // Incremento mínimo de $100
    ownerId: 'user-123' // Opcional
  }
}));

2. Pujar desde un postor

// Desde la vista del postor (bidder-asm-stream.html)
ws.send(JSON.stringify({
  kind: 'PLACE_BID',
  lotId: 'sala-100-LOT-001',
  payload: {
    amount: 5000,
    userId: 'bidder-456'
  }
}));

3. Pausar y reanudar un lote

// Pausar (por ejemplo, para aclarar algo)
ws.send(JSON.stringify({
  kind: 'PAUSE_LOT',
  lotId: 'sala-100-LOT-001'
}));

// ... después de resolver ...

// Reanudar
ws.send(JSON.stringify({
  kind: 'RESUME_LOT',
  lotId: 'sala-100-LOT-001'
}));

4. Secuencia completa de remate

// 1. Abrir lote
ws.send(JSON.stringify({
  kind: 'OPEN_LOT',
  lotId: 'sala-100-LOT-001',
  payload: { durationMs: 30000 }
}));

// 2. Los postores pujan...
// (eventos BID_PLACED se reciben automáticamente)

// 3. Primera advertencia
ws.send(JSON.stringify({
  kind: 'GO_ONCE',
  lotId: 'sala-100-LOT-001'
}));

// 4. Segunda advertencia
ws.send(JSON.stringify({
  kind: 'GO_TWICE',
  lotId: 'sala-100-LOT-001'
}));

// 5. Declarar vendido
ws.send(JSON.stringify({
  kind: 'DECLARE_AWARD',
  lotId: 'sala-100-LOT-001'
}));

// 6. Al finalizar todo el remate
ws.send(JSON.stringify({
  kind: 'FINALIZE_AUCTION'
}));
✅ Buenas Prácticas
  • Siempre verificar que el lote esté en estado válido antes de enviar comandos
  • Usar SNAPSHOT_REQUEST para sincronizar estado si hay desconexiones
  • Implementar manejo de errores para eventos BID_REJECTED
  • No olvidar llamar FINALIZE_AUCTION al terminar para grabar y archivar
  • Los botones en la UI deben habilitarse/deshabilitarse según el estado del lote