The SignalFire module provides a small implementation of the observer pattern and alternative to Roblox’s Signals. Notable differences from Signals:
- Everything is a function.
- Listeners may be threads as well as functions.
- Listeners are always deferred.
- Listeners are unordered. Relying on this was always a bad idea.
- After a signal is fired, every listener with a connection at the time of firing will be invoked.
Overview
SignalFire has several constructor functions:
The SignalFire.new constructor returns a new signal, which is represented by two functions. The first function is a “connector”, which makes connections to listeners. The second function fires the signal, invoking those listeners.
local connect, fire = SignalFire.new()
local disconnect = connect(print)
fire("Hello, world!")
--> Hello, world!
disconnect()
fire("Hello, world!")
--> (no output)
The SignalFire.wait constructor receives a connector function and turns it into a function similar to the Wait method of Roblox’s Signals.
local connect, fire = SignalFire.new()
local wait = SignalFire.wait(connect)
task.delay(1, fire, "Hello, world!")
local result = wait()
print(result)
--> Hello, world!
The SignalFire.any constructor creates a signal that fires after any one of the given signals is fired.
local connectA, fireA = SignalFire.new()
local connectB, fireB = SignalFire.new()
local connectC, fireC = SignalFire.new()
task.delay(3, function() print("Fire A") fireA("A") end)
task.delay(2, function() print("Fire B") fireB("B") end)
task.delay(1, function() print("Fire C") fireC("C") end)
local connect = SignalFire.any(connectA, connectB, connectC)
connect(function(result)
print("Fired by", result)
end)
--> Fire C
--> Fired by C
--> Fire B
--> Fire A
The SignalFire.all constructor creates a signal that fires after all of the given signals have fired.
local connectA, fireA = SignalFire.new()
local connectB, fireB = SignalFire.new()
local connectC, fireC = SignalFire.new()
task.delay(3, function() print("Fire A") fireA() end)
task.delay(2, function() print("Fire B") fireB() end)
task.delay(1, function() print("Fire C") fireC() end)
local connect = SignalFire.all(connectA, connectB, connectC)
SignalFire.wait(connect)()
print("All signals have fired")
--> Fire C
--> Fire B
--> Fire A
--> All signals have fired
The SignalFire.wrap constructor is used to convert a Roblox Signal to an API understood by SignalFire.
local bindable = Instance.new("BindableEvent")
local connectA, fireA = SignalFire.new()
local connectB = SignalFire.wrap(bindable.Event)
local connectC, fireC = SignalFire.new()
task.delay(3, function() print("Fire A") fireA() end)
task.delay(2, function() print("Fire B") bindable:Fire() end)
task.delay(1, function() print("Fire C") fireC() end)
local connect = SignalFire.all(connectA, connectB, connectC)
SignalFire.wait(connect)()
print("All signals have fired")
--> Fire C
--> Fire B
--> Fire A
--> All signals have fired
SignalFire.new also returns a destroy function, which cleans up the resources used by the signal. This should be used only after the fire function will no longer be called.
local connect, fire, destroy = SignalFire.new()
connect(function(value) print("Hello", value) end)
fire("World")
--> Hello World
destroy()
local disconnect = connect(function(value) print("Goodbye", value) end)
--> (connect does nothing but return a disconnector)
disconnect()
--> (the disconnector also does nothing)
fire("World")
--> (error) signal is destroyed
Changelog
v1.1.0 => v2.0.0
- The Fire and Destroyer functions throw an error after the signal is destroyed. The Connector function continues to behave as though the signal is not destroyed.
v1.0.0 => v1.1.0
- SignalFire.new returns a destroyer function.
- Ensure all arguments to SignalFire.any and SignalFire.all are checked.
Roadmap
- Create and run some benchmarks against other similar modules.