Cola de Mensajes Persistente (Cache)

El módulo cache proporciona un sistema de cola de mensajes con almacenamiento en SQLite, reintentos automáticos con backoff exponencial y callbacks para eventos.

Este sistema es ideal para bots y aplicaciones que necesitan garantizar la entrega de mensajes incluso en entornos con conectividad inestable.

Arquitectura

cache/
├── store.py      # MessageStore – Persistencia SQLite
├── queue.py      # MessageQueue – Lógica de cola y reintentos
├── mixin.py      # MessageQueueMixin – Integración con el cliente
└── __init__.py

1. MessageStore

Almacenamiento persistente que maneja las operaciones CRUD de mensajes en SQLite.

Estados de Mensaje (MessageStatus)

EstadoDescripción
PENDINGEsperando envío.
SENTEnviado al servidor.
DELIVEREDEntregado al destinatario.
READLeído por el destinatario.
FAILEDFalló permanentemente.
CANCELLEDCancelado por usuario.

Modelo de Datos (Message)

@dataclass
class Message:
    msg_id: str
    to: str               # JID destino
    body: str
    msg_type: str = "text"
    status: MessageStatus = MessageStatus.PENDING
    created_at: float
    sent_at: Optional[float]
    delivered_at: Optional[float]
    read_at: Optional[float]
    retry_count: int = 0
    max_retries: int = 3
    last_error: str = ""
    metadata: dict = {}

Métodos de MessageStore

MétodoDescripción
add(msg) -> boolGuarda un mensaje.
get(msg_id) -> Optional[Message]Obtiene por ID.
get_by_status(status, limit) -> List[Message]Obtiene por estado.
update_status(msg_id, new_status, error) -> boolActualiza estado.
increment_retry(msg_id) -> boolIncrementa contador de reintentos.
delete(msg_id) -> boolElimina mensaje.
get_stats() -> dictEstadísticas de la BD.
clear_old(days=30) -> intLimpia mensajes antiguos (READ y DELIVERED).

2. MessageQueue

Gestiona la cola, los reintentos automáticos y los callbacks.

Constructor

MessageQueue(store: MessageStore, auto_retry: bool = True, max_backoff: float = 300)

Métodos Principales

MétodoDescripción
enqueue(msg_id, to, body, msg_type, metadata) -> MessageAñade a la cola.
dequeue(status, limit) -> List[Message]Obtiene mensajes para procesar.
mark_sent(msg_id) -> boolMarca como enviado.
mark_delivered(msg_id) -> boolMarca como entregado.
mark_read(msg_id) -> boolMarca como leído.
mark_failed(msg_id, error) -> boolMarca como fallido.
start_auto_retry_worker()Inicia worker de reintentos.
stop_auto_retry_worker()Detiene worker.
register_callback(event, callback)Registra callback.
get_stats() -> dictEstadísticas de la cola.

Eventos Soportados

  • on_message_sent
  • on_message_delivered
  • on_message_read
  • on_message_failed

Backoff Exponencial con Jitter

Reintento 1: 1s   (±10% jitter)
Reintento 2: 2s
Reintento 3: 4s
...
Máximo: 300s (configurable)

3. MessageQueueMixin

Mixin que integra la cola en ToDusClient2 para crear ToDusClientWithQueue.

Métodos Disponibles en el Cliente

  • queue (propiedad) – Acceso a MessageQueue.
  • register_on_message_delivered(callback)
  • register_on_message_read(callback)
  • register_on_message_failed(callback)
  • get_queue_stats() -> dict
  • cleanup_queue() – Limpia mensajes antiguos (>30 días).

4. ToDusClientWithQueue

Clase final que combina ToDusClient2 y MessageQueueMixin, añadiendo el método send_message_queued().

client = ToDusClientWithQueue(
    phone_number="5312345678",
    password="password",
    enable_queue=True,
    queue_db_path="~/.todus/messages.db"
)

# Envía y encola automáticamente
msg_id = client.send_message_queued("5387654321", "Mensaje con garantía")

Comportamiento Interno:

  1. Envía el mensaje inmediatamente usando send_message().
  2. Lo agrega a la cola con estado PENDING.
  3. Si el envío es exitoso, lo marca como SENT.
  4. Si falla, el worker lo reintenta automáticamente según el backoff.
  5. Cuando se recibe la confirmación de entrega (delivered o read), se actualiza el estado y se disparan los callbacks.

Ejemplo Completo

from todus import ToDusClientWithQueue

client = ToDusClientWithQueue("5312345678", "password", enable_queue=True)
client.login()

def on_delivered(msg):
    print(f"✅ Entregado: {msg.msg_id}")

def on_failed(msg):
    print(f"❌ Falló: {msg.msg_id} - {msg.last_error}")

client.register_on_message_delivered(on_delivered)
client.register_on_message_failed(on_failed)

for i in range(10):
    client.send_message_queued("5387654321", f"Mensaje {i}")

# Ver estadísticas
stats = client.get_queue_stats()
print(stats)

Base de Datos

El esquema SQLite es el siguiente:

CREATE TABLE messages (
    msg_id TEXT PRIMARY KEY,
    "to" TEXT NOT NULL,
    body TEXT NOT NULL,
    msg_type TEXT DEFAULT 'text',
    status TEXT DEFAULT 'pending',
    created_at REAL NOT NULL,
    sent_at REAL,
    delivered_at REAL,
    read_at REAL,
    retry_count INTEGER DEFAULT 0,
    max_retries INTEGER DEFAULT 3,
    last_error TEXT DEFAULT '',
    metadata TEXT DEFAULT '{}'
);

CREATE INDEX idx_status ON messages(status);
CREATE INDEX idx_to ON messages("to");
CREATE INDEX idx_created_at ON messages(created_at);

Ruta por defecto: ~/.todus/messages.db