🎯 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.
- 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:
🎮 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
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'
}));
- Siempre verificar que el lote esté en estado válido antes de enviar comandos
- Usar
SNAPSHOT_REQUESTpara sincronizar estado si hay desconexiones - Implementar manejo de errores para eventos
BID_REJECTED - No olvidar llamar
FINALIZE_AUCTIONal terminar para grabar y archivar - Los botones en la UI deben habilitarse/deshabilitarse según el estado del lote