Extensions

Events

So far our app reacts to requests. The Events battery lets it react to things happening inside the app instead — a small publish / subscribe layer with a twist: the same events reach across to the browser through a managed WebSocket, so you can push live updates without writing any socket plumbing yourself.

Enabling events

Set EXT_EVENTS = 1. On startup the manager mounts a WebSocket at /ws/events and injects an events.js client into your pages. The depth of the internal queues is the only knob, defaulting to five:

app_configs/app.ini ini
[general]
SECRET_KEY = supersecret

[extensions]
EXT_SQLALCHEMY = 1
EXT_EVENTS = 1

[events]
EVENTS_EVENT_QUEUE_SIZE = 5

Signals, events and visibility

An event is identified by a name. Before you can trigger one, it has to exist. The simplest way to declare a pure broadcast channel is create_signal. Every event carries two flags worth understanding:

  • singleton — whether the name may only ever be registered once.
  • internal — whether the event stays server-only. Internal events are invisible to the browser; only public events (internal=False) can be subscribed to or triggered from the client.

Now that our app is structured around an app factory, event handlers deserve their own home. We'll keep them in a fluid/events package and declare a public signal that fires whenever a model is created:

fluid/events/models.py python
from webfluid.core.ext import events


# A public broadcast channel the browser is allowed to listen to.
events.create_signal("model:created", internal=False)


# A server-side reaction. internal handlers receive the event data
# and run inside a fresh framework context for you.
@events.event("model:created", internal=False)
async def on_model_created(data):
    from webfluid.utils.logging import factory as log
    log.log(f"A new model appeared: {data}")
 

Handlers must accept exactly one argument: the event data. Because the package is imported through your config / factory, declaring the signal there guarantees it exists before the first trigger fires.

Triggering an event

From anywhere in your app you publish data to a channel with events.trigger. Every registered handler runs, and every subscribed browser receives the payload. Our add_model handler is the obvious place — only it changes here, get_model stays exactly as it was in the Mail chapter:

fluid/api/models.py python
from webfluid.core.ext import db, events
from fastapi import Request
from fastapi.exceptions import HTTPException

from fluid.models import MyModel
from fluid.services.notify import welcome


async def add_model(request: Request):
    data = await request.json()
    value = data.get("value")
    if not value:
        raise HTTPException(status_code=400, detail="Bad Request")

    async with db.async_executor(model=MyModel) as e:
        model = await e.insert(MyModel(value), flush=True)

    welcome("friend@example.org", model.value)

    # Every handler runs and every subscribed browser gets the payload.
    await events.trigger("model:created", {
        "id": model.id,
        "value": model.value
    })

    return {"model_id": model.id, "value": model.value}

Reaching the browser

The browser half rides in with the surface layer we'll set up in the Frontend chapters: it injects a small events.js client that lives under window.wf.ext.events. Once that is in place, subscribing to a public event from a page takes a handful of lines — the client transparently reconnects and keeps a listen loop alive for you:

browser html
<script type="module">
    const events = new window.wf.ext.events.EventManager()

    await events.subscribe("model:created")
    events.registerHandler("model:created", (data) => {
        console.log("A model was created:", data)
    })
</script>

Queries: asking for an answer

Events are fire-and-forget. When you need a value back instead, register a query and call events.request. A singleton query returns the single handler's result; a non-singleton one collects a list from all handlers:

fluid/events/models.py python
from webfluid.core.ext import db, events
from sqlalchemy import select, func

from fluid.models import MyModel


@events.query("model:count")
async def count_models(_):
    async with db.async_executor(model=MyModel) as e:
        result = await e.exec(select(func.count(MyModel.id)), scalars=False)
        return result.scalar()


# Somewhere else in your app:
# total = await events.request("model:count")

On the server you can also consume a channel as an async stream with events.listen(name), which is handy for long-running consumers.

 

Broadcasts use a bounded per-listener buffer sized by EVENTS_EVENT_QUEUE_SIZE. A consumer that falls behind silently loses the oldest events, so treat delivery as best-effort. For dashboards and live notifications it works nicely, but do not rely on it as a durable queue yet.

Continue reading

From here you can continue straight with Cache.