Skip to content

Signals#

An opt-in, in-process event bus — connect/disconnect subscribers, deterministic delivery order, error isolation between subscribers, and always-async delivery (no sync/async choice for callers to get wrong).

Wiring a bus in#

from hayavega.addons.signals import SignalBus
from hayavega.runtime import create_application

bus = SignalBus()
bus.connect("request.finished", lambda event: print(event.data))

application = create_application(urlpatterns, signals=bus)

create_application, auth_routes, admin_routes, migrations.apply, and Entity.collection all accept signals= to emit request, authentication, administration, migration, and model lifecycle events.

Built-in events#

request.started/request.finished, auth.login_succeeded, auth.password_reset_requested/completed, auth.session_rotated, admin.action, migration.applied, websocket.connected/ disconnected/rejected (see WebSockets), model lifecycle events (below), and more — every one documented alongside the subsystem that emits it (see Authentication).

Model lifecycle events#

Entity.collection(database, signals=bus) publishes "<entity>.created"/"<entity>.updated"/"<entity>.deleted" from append/revise/erase -- the entity name lowercased, e.g. "product.created":

from hayavega.addons.signals import SignalBus

bus = SignalBus()
bus.connect("product.created", lambda event: print(event.data))  # {"id": 7}

products = Product.collection(DATABASE, signals=bus)
products.append(name="Notebook", price=9.5)

Omit signals= and nothing is published -- the same opt-in-not-on-by-default policy Hardening already follows, so every existing Entity.collection(DATABASE) call keeps working unchanged.

"<entity>.created" carries {"id": ...}. "<entity>.updated" and "<entity>.deleted" carry {"count": N} -- the number of rows a Selection.revise()/.erase() call actually touched (no event at all when count would be 0); "updated" also carries {"fields": [...]}, the column names passed to revise(), not a guaranteed before/after diff. Selections can touch many rows at once and getting a real diff would cost an extra SELECT per call, so this reports what was set, not necessarily what changed -- admin.py's own hand-written CRUD views can and do report a real diff, because they already have one record loaded for the edit form.

Delivery#

Always async — delivery never blocks the publisher. A Rust-native worker-pool dispatcher (hayavega_native.SignalDispatcher) is used when the native extension is built; a behavior-compatible Python ThreadPoolExecutor fallback is used otherwise. A subscriber that raises is logged and isolated — it never stops delivery to later subscribers or crashes the publisher.

Sensitive-key rejection#

An event's data dict is checked against a sensitive-key denylist (password, token, secret, ...) at publish time — publishing one raises, catching an accidental credential leak into the event bus before it reaches any subscriber, rather than relying on every subscriber to be careful.

A note on the name#

This was originally shipped as Pulse. hayavega.addons.pulse/pulse= still work as deprecated aliases (PulseBus is SignalBus, identity-equal, not a copy) with a single DeprecationWarning on first use — no hard break for existing code.