Dispatch contracts
- Ordinary list-backed events in the current client dispatch over a snapshot, log listener exceptions, and re-raise; one failing listener can stop later listeners.
- Safe events log listener exceptions and continue dispatch. That protects other listeners, but it does not clean up the failing listener's own state.
- Context events can store listeners under context IDs as well as an all-context sentinel. Verify whether unsubscribe removes the handler from every context or only one scope.
- Subscription collectors should be lifecycle-owned helpers. Clear them during teardown and discard the collector when the owning view or controller restarts.
Service completion lists can have different dispatch rules
A service that stores callbacks in a plain list does not inherit Event snapshot dispatch. The balance requester clears its retained request before iterating a live completion list. A reentrant listener can start new work and append a listener that receives the old result, then loses its place when the old delivery resets the list. Clearing the list during delivery can skip later listeners, and an escaping listener exception can bypass final cleanup.
Its timer processor also illustrates that canceling a pending timer does not reset the owning service's retained request field or retract a dispatched operation. For mod-owned queues, detach the completed batch before invoking consumers, keep newly submitted consumers with their own operation, and define request-state reset separately from timer cancellation. These are version-sensitive Python control-flow contracts; they do not establish a tooltip or installed-client defect.
Listener collections combine a snapshot with current membership
The client ListenersCollection helper
chains local listeners with a referenced collection. It snapshots that combined sequence, then
rechecks membership before each named callback. A pending listener removed from both owners is
skipped; one still registered through the shared collection remains eligible. New listeners wait
for another invocation. Membership uses Python equality, and registering the same listener in
both collections can deliver twice; this differs from ordinary Event snapshots.
addMutualListeners installs a one-way reference rather than transferring ownership.
Clearing the local collection removes its registrations, shared reference and accepted listener
class, while the other owner's registrations remain. Suspension is checked at invocation entry
and drops that notification without a replay queue. Suspending the referenced collection does
not filter its iterator, and clear() does not reset suspension.
Callable listener methods have per-call exception logging. Static original Python-bytecode
inspection on WG 2.4.1.1 #989 confirmed that successful calls and caught callback
exceptions skip the missing-method error; the readable dump lost that branch boundary.
Selected-source controls used recording listeners and the verified branch reconstruction.
Real prebattle/web delivery, registration order and caller-bytecode parity were not tested.
GUI event bus scope
The shared GUI event bus routes by exact event type and scope. A listener added under a battle scope does not receive the same event fired globally or from a lobby scope.
- Subscribe and remove with the same event type, handler object, and scope.
- Do not treat the default scope as proof that a copied example is correct for lobby or battle.
- Duplicate listener registration may be rejected with a warning, but mods still need their own idempotent subscription guard.
- Handler exceptions are currently isolated through safe execution. Restrictions and priorities are version-sensitive and should be used only when their ordering or gating has been verified.
- Global GUI teardown may clear the bus, but a mod should still remove its own listeners in
fini()so reloads and partial teardown are safe.
Exact handler identity
Illustrative: this authored fragment stores a callable under one module-wide key. It defines neither source.onChanged nor _handle_change, and supplies no startup or teardown lifecycle.
_handlers = {}
def _subscribe(source):
if 'changed' in _handlers:
return
def on_changed(value):
_handle_change(value)
_handlers['changed'] = on_changed
source.onChanged += on_changed
def _unsubscribe(source):
handler = _handlers.pop('changed', None)
if handler is not None:
source.onChanged -= handler
The key guards every source, so a second source is skipped while 'changed' exists. The nested function is stored before event lookup, addition and attribute writeback. A failure can leave the key suppressing a retry, with any prior side effects retained. A key is not a registration receipt.
The dictionary retains the callable, not the source or its actual event. Cleanup pops the entry before event lookup, subtraction and attribute writeback. A failure can lose the retry record while the listener remains attached. A later subscription creates a different nested function. Replacing the supplied source or event can likewise leave the original listener attached; external event clearing can instead remove membership while the key still suppresses subscription.
For the current ordinary Event, addition checks callability and list membership/equality; absent-handler removal is a no-op. A normal return need not mean a new registration or successful release of the original event. These rules do not apply automatically to every onChanged object. Retain or otherwise guarantee the same actual event and delegate through teardown; a callable ledger alone is no transaction or per-source registry.
The callback accepts one positional-or-keyword argument named value, forwards the same object as one positional argument to the module-global _handle_change, and discards its return. It captures no source, copies no payload and checks no active context. The undefined helper, incompatible event arguments or helper errors can raise. Ordinary Event logs and re-raises listener failures, stopping later snapshot listeners; SafeEvent logs and continues. Removal does not retract a callback already in a dispatch snapshot or cancel work it started.
In JavaScript, each this.method.bind(this) creates a different function. Cache it once for matching on/off calls, and separately retain the event owner and lifecycle. This is source/control-flow guidance; the fragment has not been runtime-tested and is not a complete mod.
Aggregated subscription helpers
A subscription collector is useful when one view or context owns many bindings. It remains safe only when the collector is itself scoped correctly and always cleared on teardown.