SignalFire: A small Signal alternative

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.
34 Likes

Changelog

v1.0.0 => v1.1.0

  • SignalFire.new returns a destroyer function.
  • Ensure all arguments to SignalFire.any and SignalFire.all are checked.

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.
2 Likes

Don’t know why people don’t give any reactions to the post, except for just liking. Amazing resource! Keep going ahead with your future projects!

1 Like