-
-
Save RuizuKun-Dev/93f34c9d40dd8ab7862dda39d282d157 to your computer and use it in GitHub Desktop.
Good Roblox Signal Implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local available_runner_thread = nil | |
local function acquire_runner_and_call_event_handler(callback, ...) | |
local acquired_runner = available_runner_thread | |
available_runner_thread = nil | |
callback(...) | |
available_runner_thread = acquired_runner | |
end | |
local function run_event_handler_in_available_thread() | |
while true do | |
acquire_runner_and_call_event_handler(coroutine.yield()) | |
end | |
end | |
--- @class Connection | |
--- @field IsConnected boolean | |
--- @field Disconnect function | |
export type Connection = { | |
IsConnected: boolean, | |
Disconnect: () -> void, | |
} | |
--- @class connections | |
local connections = {} | |
connections.IS_CYCLICAL = true | |
connections.__index = connections | |
--- @function create | |
--- @param signal Signal | |
--- @param callback function | |
--- @return Connection | |
--- Creates a new connection with the given signal and callback | |
function connections.create(signal: Signal, callback): Connection | |
return setmetatable({ | |
IsConnected = true, | |
_signal = signal, | |
_callback = callback, | |
_next = false, | |
USING_METATABLE = true, | |
}, connections) | |
end | |
--- @function Disconnect | |
--- Disconnects the connection if it's connected | |
function connections:Disconnect() | |
if self.IsConnected then | |
self.IsConnected = false | |
if self._signal._handler_list_head == self then | |
self._signal._handler_list_head = self._next | |
else | |
local prev = self._signal._handler_list_head | |
while prev and prev._next ~= self do | |
prev = prev._next | |
end | |
if prev then | |
prev._next = self._next | |
end | |
end | |
else | |
warn("Attempting to disconnect a connection twice") | |
end | |
end | |
--- @class Signal | |
--- @field Connect function | |
--- @field Destroy function | |
--- @field Fire function | |
--- @field Once function | |
--- @field Wait function | |
export type Signal = { | |
Connect: () -> Connection, | |
Destroy: () -> void, | |
Fire: (...any?) -> void, | |
Once: () -> Connection, | |
Wait: () -> ...any?, | |
} | |
--- @class signals | |
local signals = {} | |
signals.IS_CYCLICAL = true | |
signals.__index = signals | |
--- @function create | |
--- @return Signal | |
--- Creates a new signal | |
function signals.create(): Signal | |
return setmetatable({ | |
_handler_list_head = false, | |
USING_METATABLE = true, | |
}, signals) | |
end | |
--- @function Connect | |
--- @param callback function | |
--- @return Connection | |
--- Connects the callback to the signal and returns the connection | |
function signals:Connect(callback): Connection | |
local connection = connections.create(self, callback) | |
if self._handler_list_head then | |
connection._next = self._handler_list_head | |
self._handler_list_head = connection | |
else | |
self._handler_list_head = connection | |
end | |
return connection | |
end | |
--- @function Destroy | |
--- Clears all connections from the signal | |
function signals:Destroy() | |
self._handler_list_head = false | |
end | |
--- @function Fire | |
--- @param ... any | |
--- Fires the signal with the given arguments | |
function signals:Fire(...: any) | |
local item = self._handler_list_head | |
while item do | |
if item.IsConnected then | |
if not available_runner_thread then | |
available_runner_thread = coroutine.create(run_event_handler_in_available_thread) | |
coroutine.resume(available_runner_thread) | |
end | |
task.spawn(available_runner_thread, item._callback, ...) | |
end | |
item = item._next | |
end | |
end | |
--- @function Wait | |
--- @return ...any? | |
--- Waits for the signal to fire and returns the arguments it was fired with | |
function signals:Wait(): ...any? | |
local waiting_coroutine = coroutine.running() | |
local connection | |
connection = self:Connect(function(...) | |
connection:Disconnect() | |
task.spawn(waiting_coroutine, ...) | |
end) | |
return coroutine.yield() | |
end | |
--- @function Once | |
--- @param callback function | |
--- @return Connection | |
--- Connects the callback to the signal and disconnects it after the signal fires once | |
function signals:Once(callback): Connection | |
local connection | |
connection = self:Connect(function(...) | |
if connection.IsConnected then | |
connection:Disconnect() | |
end | |
callback(...) | |
end) | |
return connection | |
end | |
setmetatable(signals, { | |
__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 signals |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
return function() | |
local ReplicatedStorage = game:GetService("ReplicatedStorage") | |
local Utilities = ReplicatedStorage.Utilities | |
local signals = require(Utilities.Signals) | |
local test_signal = signals.create() | |
local connection1 = test_signal:Connect(function(text) | |
expect(text).to.be.equal("Fire") | |
end) | |
local whitelisted = { | |
Fire = true, | |
Disconnect = true, | |
Wait = true, | |
} | |
local connection2 = test_signal:Connect(function(text) | |
expect(whitelisted[text]).to.be.ok() | |
end) | |
it("1 - .create", function() | |
expect(signals.create).to.be.a("function") | |
expect(test_signal).to.be.a("table") | |
end) | |
describe("2 - Connection", function() | |
it("2.1 - .IsConnected", function() | |
expect(connection1.IsConnected).to.be.equal(true) | |
expect(connection2.IsConnected).to.be.equal(true) | |
end) | |
it("2.2 - :Disconnect", function() | |
connection1:Disconnect() | |
expect(connection1.IsConnected).to.be.equal(false) | |
test_signal:Fire("Disconnect") | |
end) | |
end) | |
describe("3 - Signal", function() | |
it("3.1 - .create", function() | |
expect(test_signal).to.be.ok() | |
end) | |
it("3.2 - :Wait", function() | |
expect(test_signal.Wait).to.be.a("function") | |
task.delay(1, function() | |
test_signal:Fire("Wait") | |
expect(test_signal:Wait()).to.be.equal("Wait") | |
end) | |
end) | |
it("3.3 - :Fire", function() | |
expect(test_signal.Fire).to.be.a("function") | |
test_signal:Fire("Fire") | |
end) | |
it("3.4 - :Once", function() | |
local once_signal = signals.create() | |
expect(test_signal.Once).to.be.a("function") | |
once_signal:Once(function(text) | |
expect(text).to.be.equal("Once") | |
end) | |
once_signal:Fire("Once") | |
end) | |
it("3.5 - :Destroy", function() | |
local clear_all_signal = signals.create() | |
expect(clear_all_signal.Destroy).to.be.a("function") | |
clear_all_signal:Destroy() | |
expect(clear_all_signal._handler_list_head).to.be.equal(false) | |
end) | |
end) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Revision #7