-- ROBLOX upstream: https://github.com/Roblox/roact/blob/b2ba9cf4c219c2654e6572219a68d0bf1b541418/src/SingleEventManager.lua --[[ * Copyright (c) Roblox Corporation. All rights reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ]] --[[ A manager for a single host virtual node's connected events. ]] local Packages = script.Parent.Parent.Parent.Parent local console = require(Packages.Shared).console type Function = (...any) -> ...any 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", } export type EventManager = { connectPropertyChange: (self: any, eventName: string, newValue: any) -> (), connectEvent: (self: any, eventName: string, newValue: any) -> (), resume: (self: any) -> (), suspend: (self: any) -> (), } local SingleEventManager = {} SingleEventManager.__index = SingleEventManager function SingleEventManager.new(instance: Instance): EventManager 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 :: any) :: EventManager end function SingleEventManager:connectEvent(key, listener) self:_connect(key, self._instance[key], listener) end function SingleEventManager:connectPropertyChange(key, listener) local success, event = pcall(self._instance.GetPropertyChangedSignal, self._instance, key) if not success then error( string.format( "Cannot get changed signal on property %q: %s", 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 -- More events might be added to the queue when evaluating events, so we -- need to be careful in order to preserve correct evaluation order. for _, eventInvocation in self._suspendedEventQueue do 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 console.warn("%s", result) end end end self._isResuming = false self._status = EventStatus.Enabled table.clear(self._suspendedEventQueue) end return SingleEventManager