Saltar a contenido

Mensajes tipados

Los dicts planos funcionan en todo lapinbeam, pero rara vez quieres msg.get("type") == "ACK" esparcido por el código de tus actores cuando Python ya tiene tipos reales. Dos piezas independientes hacen esto cómodo: payloads que preservan el tipo (dataclasses y modelos Pydantic sobreviven el viaje entre nodos) y despacho tipado (@on(Type), una alternativa a un único método receive).

Payloads que preservan el tipo

lapinbeam.codec envuelve instancias @dataclass y modelos Pydantic v2 en un sobre etiquetado ({"__lb_type__": "module.QualName", "data": {...}}) antes de que lleguen al transporte Rust, que solo acepta JSON, y reconstruye el tipo exacto en el extremo receptor:

from dataclasses import dataclass
from lapinbeam import Node, Supervisor, actor


@dataclass
class Task:
    payload_id: int
    name: str


@actor(name="worker")
class Worker:
    async def receive(self, msg: Task):
        print(msg.payload_id, msg.name)  # msg es un Task real, no un dict


# El envío es transparente — sin paso manual de serialización:
# await remote.send(Task(payload_id=1, name="build"))

Esto funciona igual para modelos Pydantic v2. Las clases propias (cualquier cosa que no sea un dataclass ni un modelo Pydantic) necesitan un códec explícito:

from lapinbeam import register_codec

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

register_codec(
    Point,
    encode=lambda p: {"x": p.x, "y": p.y},
    decode=lambda d: Point(d["x"], d["y"]),
)

Ambos extremos del clúster deben registrar el mismo códec — el tipo se busca por su etiqueta module.QualName, así que la clase debe ser importable (o estar registrada) allí donde corra decode_payload.

Los envíos locales siempre son sin copia

La preservación de tipo solo importa en envíos remotos. Un ActorRef.send(obj) local pasa el objeto Python exacto por referencia — sin codificar, sin copiar, y sin restricción a tipos compatibles con JSON o cubiertos por un códec. Puedes enviar cualquier cosa entre dos actores del mismo nodo.

Despacho tipado con @on

Una vez los mensajes llevan tipos reales, despachar a mano sigue significando una cadena de comprobaciones isinstance/match dentro de un único receive. @on(Type) mueve eso a la propia declaración del actor:

from dataclasses import dataclass
from lapinbeam import actor, on


@dataclass
class Task:
    payload_id: int
    name: str


@dataclass
class Ack:
    result: int


@actor(name="worker")
class Worker:
    @on(Task)
    async def handle_task(self, msg: Task):
        ...

    @on(Ack)
    async def handle_ack(self, msg: Ack):
        ...

    @on(default=True)
    async def handle_other(self, msg):
        print("mensaje no reconocido:", msg)

Un actor con cualquier handler @on deja de usar receive por completo — los mensajes se despachan por type(msg) al handler correspondiente. @on(default=True) marca un único handler de "cajón de sastre" para cualquier tipo sin handler dedicado (incluidos los dicts planos); es la forma más sencilla de estar a salvo frente a formas de mensaje que no planeaste. Sin un handler por defecto, un tipo sin coincidencia lanza TypeError, lo que hace fallar al actor y deja que Supervisor lo reinicie — la misma filosofía "let it crash" que ya sigue el resto del framework para excepciones no controladas en receive.

Los actores que solo definen receive no se ven afectados en absoluto — @on es estrictamente aditivo, no una migración forzosa.

O prescinde de @on por completo: usa match

Como el mensaje ya llega con su tipo real, no necesitas @on para tener despacho tipado — el pattern matching estructural de Python funciona directamente sobre dataclasses dentro de un único receive:

async def receive(self, msg):
    match msg:
        case Task(payload_id=pid, name=name):
            ...
        case Ack(result=r):
            ...
        case _:
            print("mensaje no reconocido:", msg)

Esto no cuesta nada (ninguna característica del framework está involucrada) y da destructuring en el mismo aliento que la comprobación de tipo. Recurre a @on en su lugar cuando prefieras tener un método pequeño por tipo de mensaje — por ejemplo, para testear cada handler de forma aislada, o cuando un único bloque match crece demasiado.