Publish events¶
Plugin.emit() publishes an event. The name is delivered verbatim — no prefix is
added by the framework.
Publish a basic event¶
- Define your plugin class by subclassing
Plugin. - Call
await self.emit(name, data)wherever you want to signal an occurrence.
from uxok import Plugin
class CounterPlugin(Plugin):
async def on_start(self) -> None:
await self.emit("counter.started", {"initial": 0})
async def increment(self, value: int) -> None:
await self.emit("counter.incremented", {"value": value})
Any subscriber registered to "counter.incremented" receives this event. The name
you pass to emit() is the name subscribers match against — exactly as written.
Subscribe to an event in the same or another plugin¶
- Decorate a method with
@event(name)to receive the event.
from uxok import Plugin, event
class LoggerPlugin(Plugin):
@event("counter.incremented")
async def handle(self, ev) -> None:
print(f"value: {ev.data['value']}")
The @event decorator registers the handler during Plugin.start(). Cleanup
happens automatically when the plugin stops or reloads.
Read the emitter's identity from Event.source¶
- Access
ev.sourcein the handler to identify which plugin sent the event.
class LoggerPlugin(Plugin):
@event("counter.incremented")
async def handle(self, ev) -> None:
print(f"from: {ev.source}") # "counter_plugin"
print(f"value: {ev.data['value']}")
Plugin.emit() stamps Event.source with the emitting plugin's name. You do not
need to encode the sender into the topic. Event.source is None when an event is
published directly via core.events.publish() rather than through Plugin.emit().
Skip emission when nobody is listening¶
- Check
self.has_subscribers(name)before constructing an expensive payload.
class MetricsPlugin(Plugin):
async def report(self) -> None:
if self.has_subscribers("metrics.snapshot"):
payload = self._build_snapshot() # only called when needed
await self.emit("metrics.snapshot", payload)
has_subscribers() is mute-aware: it returns False when the topic is suppressed,
so the demand gate skips muted topics automatically. Plugin.emit() already
short-circuits internally when there are no subscribers, but the guard is useful
when payload construction itself is costly.
Defer emission to a future tick¶
- Pass
at_tickto schedule the event for a specific tick boundary.
class SchedulerPlugin(Plugin):
async def queue_work(self) -> None:
target = self.core.tick + 500
await self.emit("work.due", {"task": "cleanup"}, at_tick=target)
emit(at_tick=...) returns immediately — the event is queued fire-and-forget.
at_tick must be strictly greater than self.core.tick at the moment of the call.
Passing a tick in the past raises ValueError.
# Raises ValueError — tick is already past
await self.emit("work.due", {}, at_tick=self.core.tick - 10)
Always compute the target as self.core.tick + N to keep it in the future.
Implement a recurring event¶
- Schedule the next occurrence from inside the handler — there is no built-in recurrence primitive.
INTERVAL = 100 # ticks
class HeartbeatPlugin(Plugin):
async def on_start(self) -> None:
await self.emit("heartbeat", {}, at_tick=self.core.tick + INTERVAL)
@event("heartbeat")
async def handle_heartbeat(self, ev) -> None:
# periodic work here
await self.emit("heartbeat", {}, at_tick=self.core.tick + INTERVAL)
Read tick metadata from an event¶
Events published after the core starts carry timing context stamped by the bus.
class ObserverPlugin(Plugin):
@event("counter.incremented")
async def observe(self, ev) -> None:
print(f"name: {ev.name}") # "counter.incremented"
print(f"source: {ev.source}") # emitting plugin name, or None
print(f"tick: {ev.tick}") # tick when published; 0 before core starts
print(f"slip: {ev.slip}") # timing drift in tick periods; 0 = on-schedule
print(f"data: {ev.data}")
tick and slip are 0 when the tick system has not started. timestamp is
always present (set to wall-clock time at construction).
For how dispatch works and why the bus uses fire-and-forget concurrency, see Event system.