Skip to content

Instantly share code, notes, and snippets.

@RuizuKun-Dev
Forked from stravant/GoodSignal.lua
Last active June 3, 2023 01:00
Show Gist options
  • Save RuizuKun-Dev/93f34c9d40dd8ab7862dda39d282d157 to your computer and use it in GitHub Desktop.
Save RuizuKun-Dev/93f34c9d40dd8ab7862dda39d282d157 to your computer and use it in GitHub Desktop.
Good Roblox Signal Implementation
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
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
@RuizuKun-Dev
Copy link
Author

RuizuKun-Dev commented Sep 23, 2021

Revision #2

  • fixed Linter (Selene) problems
  • renamed fn to callback
  • renamed Connection._connected to Connection.Connected and made it public
  • removed comments

@RuizuKun-Dev
Copy link
Author

RuizuKun-Dev commented Sep 23, 2021

Revision #3

  • instead of erroring when attempting to disconnect a connection twice we warn instead
  • changed Connection.new to Connection.create

@RuizuKun-Dev
Copy link
Author

Revision #4

  • changed Signal.new to Signal.create

@RuizuKun-Dev
Copy link
Author

Revision #5

  • Fixed bug from Revision #3 "instead of erroring when attempting to disconnect a connection twice we warn instead"

@RuizuKun-Dev
Copy link
Author

RuizuKun-Dev commented Sep 1, 2022

Revision #6

  • Once added

@RuizuKun-Dev
Copy link
Author

RuizuKun-Dev commented Jun 3, 2023

Revision #7

  • Refactor code to follow standardize conventions and style guides
  • Added Moonwave Doc
  • Added Comments as Documentation
  • Added Signal.spec.lua

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment