--[[ A manager for a single host virtual node's connected events. ]] local Logging = require(script.Parent.Logging) local CHANGE_PREFIX = "Change." local EventStatus = { -- No events are processed at all; they're silently discarded Disabled = "Disabled", -- Events are stored in a queue; listeners are invoked when the manager is resumed Suspended = "Suspended", -- Event listeners are invoked as the events fire Enabled = "Enabled", } local SingleEventManager = {} SingleEventManager.__index = SingleEventManager function SingleEventManager.new(instance) local self = setmetatable({ -- The queue of suspended events _suspendedEventQueue = {}, -- All the event connections being managed -- Events are indexed by a string key _connections = {}, -- All the listeners being managed -- These are stored distinctly from the connections -- Connections can have their listeners replaced at runtime _listeners = {}, -- The suspension status of the manager -- Managers start disabled and are "resumed" after the initial render _status = EventStatus.Disabled, -- If true, the manager is processing queued events right now. _isResuming = false, -- The Roblox instance the manager is managing _instance = instance, }, SingleEventManager) return self end function SingleEventManager:connectEvent(key, listener) self:_connect(key, self._instance[key], listener) end function SingleEventManager:connectPropertyChange(key, listener) local success, event = pcall(function() return self._instance:GetPropertyChangedSignal(key) end) if not success then error(("Cannot get changed signal on property %q: %s"):format( tostring(key), event ), 0) end self:_connect(CHANGE_PREFIX .. key, event, listener) end function SingleEventManager:_connect(eventKey, event, listener) -- If the listener doesn't exist we can just disconnect the existing connection if listener == nil then if self._connections[eventKey] ~= nil then self._connections[eventKey]:Disconnect() self._connections[eventKey] = nil end self._listeners[eventKey] = nil else if self._connections[eventKey] == nil then self._connections[eventKey] = event:Connect(function(...) if self._status == EventStatus.Enabled then self._listeners[eventKey](self._instance, ...) elseif self._status == EventStatus.Suspended then -- Store this event invocation to be fired when resume is -- called. local argumentCount = select("#", ...) table.insert(self._suspendedEventQueue, { eventKey, argumentCount, ... }) end end) end self._listeners[eventKey] = listener end end function SingleEventManager:suspend() self._status = EventStatus.Suspended end function SingleEventManager:resume() -- If we're already resuming events for this instance, trying to resume -- again would cause a disaster. if self._isResuming then return end self._isResuming = true local index = 1 -- More events might be added to the queue when evaluating events, so we -- need to be careful in order to preserve correct evaluation order. while index <= #self._suspendedEventQueue do local eventInvocation = self._suspendedEventQueue[index] local listener = self._listeners[eventInvocation[1]] local argumentCount = eventInvocation[2] -- The event might have been disconnected since suspension started; in -- this case, we drop the event. if listener ~= nil then -- Wrap the listener in a coroutine to catch errors and handle -- yielding correctly. local listenerCo = coroutine.create(listener) local success, result = coroutine.resume( listenerCo, self._instance, unpack(eventInvocation, 3, 2 + argumentCount)) -- If the listener threw an error, we log it as a warning, since -- there's no way to write error text in Roblox Lua without killing -- our thread! if not success then Logging.warn("%s", result) end end index = index + 1 end self._isResuming = false self._status = EventStatus.Enabled self._suspendedEventQueue = {} end return SingleEventManager