mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
This commit is contained in:
parent
fab66a9d6b
commit
574f4400bc
21 changed files with 224 additions and 337 deletions
182
test/goodsignal.luau
Normal file
182
test/goodsignal.luau
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
--!nocheck
|
||||
-- wrapped task library for use in pure luau
|
||||
local task = { spawn = function(thread, ...)
|
||||
local ok, err = coroutine.resume(thread, ...)
|
||||
if not ok then error(err, 3) end
|
||||
end }
|
||||
|
||||
export type Type = RBXScriptSignal & { Fire: (Type, ...any)-> () }
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- 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(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
|
||||
|
||||
Signal.__type = "RBXScriptSignal"
|
||||
|
||||
function Signal.new(): Type
|
||||
return setmetatable({
|
||||
_handlerListHead = false,
|
||||
}, Signal) :: any
|
||||
end
|
||||
|
||||
function Signal:Connect(fn)
|
||||
if type(fn) ~= "function" then error(`attempt to connect non function (got { type(fn) })`, 2) end
|
||||
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
|
||||
|
||||
return Signal
|
||||
Loading…
Add table
Add a link
Reference in a new issue