This site works best with JavaScript enabled. Please enable JavaScript to get the best experience from this site.
First, thank you for maintaining this project; it has been a cornerstone of the ecosystem for a long time.
I’ve unfortunately identified an issue in CallbackHandler that causes events to be silently dropped. In environments where this library is shared across multiple independent addons (such as through AceAddon), this creates unpredictable behavior that is extremely difficult to debug.
CallbackHandler
The current logic uses an insertQueue to prevent table mutation during iteration, which is an excellent idea. However, because the queue is only processed afterwords (when oldrecurse == 0), any events fired recursively (while a dispatch is already in progress) will not be seen by callbacks currently sitting in the queue, and the messages are lost.
insertQueue
oldrecurse == 0
This is a minimal example for non-determenistic behavior:
function example() MyCallbacks:RegisterMessage('SOME_EVENT', function() print('Event received!') end) MyCallbacks:FireMessage('SOME_EVENT') end
Whether "Event received!" prints depends entirely on whether example() call results indirectly or not from another Fire execution:
If example() is called independently, it works as expected.
example()
If example() is triggered by a different callback (recursion), the new registration stays in insertQueue until the top-level dispatch finishes, causing the FireMessage to be "lost".
FireMessage
Notice that this is still true if Addon A registers an event and Addon B fires it, this is just the simplest example.
Because CallbackHandler is a shared dependency, the recursion is often "global" across the entire UI:
Unpredictability: A developer cannot know if their code is being executed due to a Fire call from a completely unrelated addon.
Fire
Silent Failure: If Addon A triggers a callback that causes Addon B to initialize and fire an event (for example it initializes a new frame which needs to register events), Addon B will fail to receive its own events, with no error thrown.
Non-determinism: The behavior changes based on which addon happened to trigger which execution chain of events, making it nearly impossible to reproduce consistently.
There are likely several ways to approach a fix, here's my suggestion to help with it: during recursion, treat tables as immutable, and do a table swap instead of a queue, framebuffers style. This idea would likely be more efficient if recursion was tracked per-event as well, instead of globally.
recurse = {}<br />target[RegisterName] = function(self, eventname, method, ...) -- ... the current type check, regfunc closure creation code if (recurse[eventname] or 0) > 0 then -- Create a new version of the table so existing iterators aren't disturbed events[eventname] = CopyTable(events[eventname]) end events[eventname][self] = regfunc if registry.OnUsed and first then registry.OnUsed(registry, target, eventname) end<br />end
function registry:Fire(eventname, ...) if not rawget(events, eventname) or not next(events[eventname]) then return end local oldrecurse = recurse[eventname] or 0 recurse[eventname] = oldrecurse + 1 Dispatch(events[eventname], eventname, ...) -- dispatch will use the current version of the table, which is immutable (and thus safe) recurse[eventname] = oldrecurse -- insertQueue logic is now gone end
To post a comment, please login or register a new account.