A lightweight signal/event class for Luau — drop-in alternative to BindableEvent with a friendlier API and lower overhead.
local sig = Signal.new()
sig:Connect(function(value)
print("got", value)
end)
sig:Fire(42) --> got 42BindableEvent is fine, but it allocates a fresh thread per handler invocation and serialises arguments. For in-Lua eventing — gameplay systems, internal pub/sub, custom service signals — a pure-Lua signal is faster and lets you pass any value (tables, functions, mixed types) without copying.
This module uses the free-runner thread pattern: a single coroutine is reused across handler calls, so a non-yielding handler costs effectively zero allocations.
[dependencies]
Signal = "rumin/signal@1.0.0"Copy src/ into your project and rename it to Signal. Or point Rojo at the included default.project.json to mount the folder as a ModuleScript.
Create a new signal.
Register a handler. Returns a Connection. Handlers connected during a Fire call are not invoked by that same call.
Like Connect, but the handler is disconnected after the first invocation.
Yield the current thread until the next Fire. Returns the arguments that were fired.
Invoke every connected handler synchronously, in connection order (newest first). Each handler runs on a reused coroutine; if a handler yields, subsequent handlers get a fresh coroutine.
Like Fire, but each handler is dispatched via task.defer — runs after the current resumption cycle completes.
Disconnect every handler. The signal remains usable; Destroy is just an alias for symmetry with Roblox conventions.
Read-only flag.
Disconnect this handler. Safe to call multiple times.
local Signal = require(ReplicatedStorage.Signal)
local playerScored = Signal.new()
local conn = playerScored:Connect(function(name, points)
print(("%s scored %d points"):format(name, points))
end)
playerScored:Once(function(name)
print(name .. " was the first to score!")
end)
playerScored:Fire("Alice", 10) -- prints both lines
playerScored:Fire("Bob", 5) -- only the persistent one
conn:Disconnect()See examples/basic.lua for a fuller walkthrough.
- Iteration safety. Disconnecting a handler during
Fireis safe; the disconnected handler is skipped on the same pass. Handlers added duringFireare not invoked until the nextFire. - Argument passthrough. Arguments are passed through
task.spawn, so any Lua value works (tables stay as references, no serialisation). - Yielding handlers. A handler may yield. The next handler runs on a fresh coroutine; the yielded handler eventually resumes independently.
MIT — see LICENSE.