local P=nil local function z(z,...)local g=P P=nil z(...)P=g end local function g(...)z(...)while true do z(coroutine.yield())end end local q={}q.__index=q function q.new(P,z)return setmetatable({_connected=true;_signal=P;_fn=z,_next=false},q)end function q.Disconnect(P)assert(P._connected,"\067\097\110\'\116 \100\105\115\099\111\110\110\101\099\116 \097 \099\111\110\110\101\099\116\105\111\110 \116\119\105\099\101\046",2)P._connected=false local z=P._signal if z._handlerListHead==P then z._handlerListHead=P._next else local g=z._handlerListHead while g and g._next~=P do g=g._next end if g then g._next=P._next end end if z.connectionsChanged then z.totalConnections-=1 z.connectionsChanged:Fire(-1)end end setmetatable(q,{__index=function(P,z)error(("\065\116\116\101\109\112\116 \116\111 \103\101\116 \067\111\110\110\101\099\116\105\111\110\058\058\037\115 \040\110\111\116 \097 \118\097\108\105\100 \109\101\109\098\101\114\041"):format(tostring(z)),2)end;__newindex=function(P,z,g)error(("\065\116\116\101\109\112\116 \116\111 \115\101\116 \067\111\110\110\101\099\116\105\111\110\058\058\037\115 \040\110\111\116 \097 \118\097\108\105\100 \109\101\109\098\101\114\041"):format(tostring(z)),2)end})local e={}e.__index=e function e.new(P)local z=setmetatable({_handlerListHead=false},e)if P then z.totalConnections=0 z.connectionsChanged=e.new()end return z end function e.Connect(z,P)local g=q.new(z,P)if z._handlerListHead then g._next=z._handlerListHead z._handlerListHead=g else z._handlerListHead=g end if z.connectionsChanged then z.totalConnections+=1 z.connectionsChanged:Fire(1)end return g end function e.DisconnectAll(P)P._handlerListHead=false if P.connectionsChanged then P.connectionsChanged:Fire(-P.totalConnections)P.connectionsChanged:Destroy()P.connectionsChanged=nil P.totalConnections=0 end end e.Destroy=e.DisconnectAll e.destroy=e.DisconnectAll function e.Fire(z,...)local q=z._handlerListHead while q do if q._connected then if not P then P=coroutine.create(g)end task.spawn(P,q._fn,...)end q=q._next end end function e.Wait(P)local z=coroutine.running()local g g=P:Connect(function(...)g:Disconnect()task.spawn(z,...)end)return coroutine.yield()end return e
-- #selene: allow(unused_variable)
-- #selene: allow(incorrect_standard_library_use)

--------------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- API:                                                                       --
--   local Signal = require(THIS MODULE)                                      --
--   local sig = Signal.new()                                                 --
--   local connection = sig:Connect(function(arg1, arg2, ...) ... end)        --
--   sig:Fire(arg1, arg2, ...)                                                --
--   connection:Disconnect()                                                  --
--   sig:DisconnectAll()                                                      --
--   local arg1, arg2, ... = sig:Wait()                                       --
--                                                                            --
-- Licence:                                                                   --
--   Licenced under the MIT licence.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--------------------------------------------------------------------------------

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection.new(signal, fn)
	return setmetatable({
		_connected = true,
		_signal = signal,
		_fn = fn,
		_next = false,
	}, Connection)
end

function Connection:Disconnect()
	assert(self._connected, "Can't disconnect a connection twice.", 2)
	self._connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	local signal = self._signal
	if signal._handlerListHead == self then
		signal._handlerListHead = self._next
	else
		local prev = signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end

	if signal.connectionsChanged then
		signal.totalConnections -= 1
		signal.connectionsChanged:Fire(-1)
	end
end

-- Make Connection strict
setmetatable(Connection, {
	__index = function(tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(tb, key, value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

-- Signal class
local Signal = {}
Signal.__index = Signal

function Signal.new(createConnectionsChangedSignal)
	local self = setmetatable({
		_handlerListHead = false,
	}, Signal)
	if createConnectionsChangedSignal then
		self.totalConnections = 0
		self.connectionsChanged = Signal.new()
	end
	return self
end

function Signal:Connect(fn)
	local connection = Connection.new(self, fn)
	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end

	if self.connectionsChanged then
		self.totalConnections += 1
		self.connectionsChanged:Fire(1)
	end
	return connection
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
function Signal:DisconnectAll()
	self._handlerListHead = false

	if self.connectionsChanged then
		self.connectionsChanged:Fire(-self.totalConnections)
		self.connectionsChanged:Destroy()
		self.connectionsChanged = nil
		self.totalConnections = 0
	end
end
Signal.Destroy = Signal.DisconnectAll
Signal.destroy = Signal.DisconnectAll

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item._connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

-- Implement Signal:Wait() in terms of a temporary connection using
-- a Signal:Connect() which disconnects itself.
function Signal:Wait()
	local waitingCoroutine = coroutine.running()
	local cn
	cn = self:Connect(function(...)
		cn:Disconnect()
		task.spawn(waitingCoroutine, ...)
	end)
	return coroutine.yield()
end

return Signal
