Documentación

Inicio rápido

Yoltra es un contenedor de estado orientado a eventos y agnóstico al framework, con suscripciones reactivas de grano fino. Tú escribes eventos; los reducers los pliegan en el estado; una comparación estructural notifica a los suscriptores exactamente en las rutas que cambiaron; los efectos corren después. Sin proxies, sin magia — solo diff(prev, next) → rutas → suscriptores.

#Instalación

Yoltra requiere CPython ≥ 3.11 y no tiene dependencias de ejecución.

bash
pip install yoltra

#Tu primer store

python
from yoltra import create_store

store = create_store("Todos")

# Un reducer es dueño de un slice del estado y pliega en él los eventos que coincidan.
@store.reducer(name="todos", state={"items": []}, when={"keys": [("ui", "add")]})
def todos(prev, event):
    return {"items": [*prev["items"], {"title": event.payload, "done": False}]}

# Emite un evento. El estado se confirma de forma síncrona — en el instante en que emit() retorna.
store.emit("ui", "add", "Buy milk")

print(store.get_state()["todos"]["items"])
# [{'title': 'Buy milk', 'done': False}]

Pasaron tres cosas: emit("ui", "add", "Buy milk") creó un evento; el reducer todos (registrado para la clave ("ui", "add")) produjo el siguiente slice; y el store lo confirmó antes de que emit retornara.

#Reacciona a los cambios con connect

connect se suscribe a una ruta con puntos dentro de un slice. El store compara cada cambio y dispara solo a los suscriptores cuya ruta cambió:

python
store.connect(
    reducer="todos",
    property="items",
    handler=lambda change: print("items changed:", change.new_value),
)

store.emit("ui", "add", "Walk the dog")
# items changed: [{'title': 'Buy milk', ...}, {'title': 'Walk the dog', ...}]

Las rutas admiten globs: * coincide con un segmento, ** coincide con cualquier profundidad. Un monitor sobre items.*.title se dispara cuando cambia el título de cualquier item:

python
store = create_store("Todos")

@store.reducer(name="todos", state={"items": []},
               when={"keys": [("ui", "add"), ("ui", "rename")]})
def todos(prev, event):
    if event.type == "add":
        return {"items": [*prev["items"], {"title": event.payload, "done": False}]}
    # rename: (índice, nuevo_título)
    i, title = event.payload
    items = list(prev["items"])
    items[i] = {**items[i], "title": title}
    return {"items": items}

store.connect(reducer="todos", property="items.*.title",
              handler=lambda c: print(f"title {c.path}: {c.old_value!r}{c.new_value!r}"))

store.emit("ui", "add", "Buy milk")
store.emit("ui", "rename", (0, "Buy oat milk"))
# title items.0.title: 'Buy milk' → 'Buy oat milk'

Un detalle que conviene saber pronto: agregar un item cambia la longitud de la lista, así que la comparación marca la ruta completa items (no items.0.title). Una edición en el lugar de un item existente (misma longitud) produce la ruta hoja precisa. Esto se desprende de la comparación estructural y se explica en la guía para desarrolladores.

#Ejecuta efectos secundarios

Los efectos corren después de que un evento se confirma — ideales para persistencia, logging o disparar la siguiente acción. Pueden ser síncronos o asíncronos:

python
@store.effect(when={"keys": [("ui", "add")]})
def persist(event, get_state, emit):
    save_to_disk(get_state()["todos"]["items"])   # tu código

emit() retorna un EmitHandle. Desde código síncrono puedes bloquear hasta que el efecto termine; desde código asíncrono puedes esperarlo:

python
handle = store.emit("ui", "add", "Buy milk")
handle.wait()                 # síncrono: bloquea hasta que terminen los efectos de este evento

# o, dentro de código async:
# await store.emit("ui", "add", "Buy milk")

#Protege las escrituras con middleware

El middleware corre antes de los reducers y puede vetar un evento retornando False:

python
@store.middleware()
def reject_empty(state, event, emit):
    return not (event.type == "add" and event.payload == "")

store.emit("ui", "add", "")     # vetado — no se agrega ningún item

#Hacia dónde seguir

Editar esta página en GitHub