local A=nil local function a(a,...)local b=A A=nil a(...)A=b end local function b()while true do a(coroutine.yield())end end local s={}s.__index=s function s.new(A,a)return setmetatable({_connected=true,_signal=A;_fn=a;_next=false},s)end function s.Disconnect(A)A._connected=false if A._signal._handlerListHead==A then A._signal._handlerListHead=A._next else local a=A._signal._handlerListHead while a and a._next~=A do a=a._next end if a then a._next=A._next end end end setmetatable(s,{__index=function(A,a)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(a)),2)end,__newindex=function(A,a)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(a)),2)end})local m={}m.__index=m function m.new()return setmetatable({_handlerListHead=false},m)end function m.Connect(a,A)local b=s.new(a,A)if a._handlerListHead then b._next=a._handlerListHead a._handlerListHead=b else a._handlerListHead=b end return b end function m.DisconnectAll(A)A._handlerListHead=false end function m.Fire(a,...)local s=a._handlerListHead while s do if s._connected then if not A then A=coroutine.create(b)coroutine.resume(A)end task.spawn(A,s._fn,...)end s=s._next end end function m.Wait(A)local a=coroutine.running()local b b=A:Connect(function(...)b:Disconnect()task.spawn(a,...)end)return coroutine.yield()end function m.Once(a,A)local b b=a:Connect(function(...)if b._connected then b:Disconnect()end A(...)end)return b end setmetatable(m,{__index=function(A,a)error(("\065\116\116\101\109\112\116 \116\111 \103\101\116 \083\105\103\110\097\108\058\058\037\115 \040\110\111\116 \097 \118\097\108\105\100 \109\101\109\098\101\114\041"):format(tostring(a)),2)end;__newindex=function(A,a)error(("\065\116\116\101\109\112\116 \116\111 \115\101\116 \083\105\103\110\097\108\058\058\037\115 \040\110\111\116 \097 \118\097\108\105\100 \109\101\109\098\101\114\041"):format(tostring(a)),2)end})return m
--------------------------------------------------------------------------------
--               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()
	-- Note: We cannot use the initial set of arguments passed to
	-- runEventHandlerInFreeThread for a call to the handler, because those
	-- arguments would stay on the stack for the duration of the thread's
	-- existence, temporarily leaking references. Without access to raw bytecode
	-- there's no way for us to clear the "..." references from the stack.
	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()
	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.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_, key)
		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()
	return setmetatable({
		_handlerListHead = false,
	}, Signal)
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
	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
end

-- 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)
				-- Get the freeRunnerThread to the first yield
				coroutine.resume(freeRunnerThread)
			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

-- Implement Signal:Once() in terms of a connection which disconnects
-- itself before running the handler.
function Signal:Once(fn)
	local cn
	cn = self:Connect(function(...)
		if cn._connected then
			cn:Disconnect()
		end
		fn(...)
	end)
	return cn
end

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

return Signal
