-
Notifications
You must be signed in to change notification settings - Fork 0
API Signal
Signal is the framework's lightweight event primitive: connect handlers, fire payloads, wait for the next occurrence, and destroy all listeners without using BindableEvent.
Important
Docs audit note (2026-05): This page was re-audited against FSM/Orchestrator/Core/Signal.luau.
The implementation is intentionally tiny:
- handlers live in
self._handlers -
Connect()stores connection tables with_handler,_signal, andDisconnect() -
Fire(...)snapshots the current connection array withtable.move(...) - each handler runs in its own
task.spawn(...) -
Wait()is implemented withOnce(...)+coroutine.yield()
That gives you Roblox-signal-like ergonomics with explicit lifecycle control.
Signal does not export a separate module for connections, but Connect() / Once() return objects with this effective shape:
type Connection = {
_handler: (...any) -> (),
_signal: any,
Disconnect: (self: Connection) -> (),
}Disconnect():
- looks up itself in
signal._handlers - removes that entry if found
- sets
conn._handler = nil - sets
conn._signal = nil
Calling Disconnect() twice is safe.
Creates a new signal with an empty _handlers array.
local sig = Signal.new()Registers a persistent handler.
Side effects
- appends a new connection table to
_handlers
Edge cases
- the same function can be connected multiple times; each call returns an independent connection
- later connections are stored later in the array, but
Fire()iterates the snapshot backward
local conn = sig:Connect(function(value)
print("received", value)
end)Registers a handler that disconnects itself before invoking the wrapped callback.
Behavior
- internally calls
Connect(function(...) connection:Disconnect(); handler(...) end) - fires at most once unless you register a new
Once()handler later
sig:Once(function(state)
print("first transition only", state)
end)Invokes the current listeners asynchronously.
Implementation details
- snapshots
self._handlerswithtable.move - iterates that snapshot from back to front
- only spawns handlers whose
conn._handleris still non-nil
Why the snapshot matters
- disconnecting a listener during a fire does not corrupt iteration
- if an earlier handler disconnects a later connection, that later snapshot entry now has
_handler = nil, so it is skipped -
Destroy()clearsself._handlers, but handlers already captured in the snapshot may still run becauseDestroy()does not nil_handler
sig:Fire(42, "hello")Yields until the next fire, then returns that payload.
Behavior
- captures
coroutine.running() - registers a
Once()handler that resumes the waiting thread viatask.spawn(thread, ...) - returns
coroutine.yield()
Edge cases
- there is no built-in timeout
- only future fires count; earlier fires are not queued
local a, b = sig:Wait()Clears the signal's live listener list.
Side effects
- sets
conn._signal = nilfor every currently registered connection table.clear(self._handlers)
Important
- existing connection objects become inert because
_signalis gone - the signal table itself still exists and can be reused with future
Connect()calls
sig:Destroy()| Method | Signature | Returns | Notes |
|---|---|---|---|
new |
() -> Signal |
Signal |
creates empty handler list |
Connect |
((...any) -> ()) -> Connection |
Connection |
persistent listener |
Once |
((...any) -> ()) -> Connection |
Connection |
auto-disconnects before callback |
Fire |
(...any) -> () |
() |
snapshots listener array and spawns each handler |
Wait |
() -> ...any |
...any |
waits for next fire only |
Destroy |
() -> () |
() |
clears current live listeners |
local conn = self.StateUpdated:Connect(function(changes)
if changes.Health then
updateHealthBar(changes.Health)
end
end)
self:Manage(conn)self:Manage(self.Destroyed:Once(function()
print(self.Name, "destroyed exactly once")
end))local function waitWithTimeout(signal, timeoutSeconds)
local done = false
local payload = nil
signal:Once(function(...)
done = true
payload = table.pack(...)
end)
local deadline = os.clock() + timeoutSeconds
while not done and os.clock() < deadline do
task.wait()
end
return done, payload
end-
Wait()can leak suspended threads. Always wrap it when the signal may never fire. -
Handlers do not run inline.
task.spawn(...)means you cannot rely on synchronous ordering between listeners. -
Recursive self-fire is still possible. Snapshotting protects iteration, not logic.
-
Destroy()clears only currently registered listeners. A fire already in progress may still finish spawning callbacks captured in its snapshot.
Quick Links: Home Β· Quick Start Β· API Reference Β· Architecture Β· Examples Β· Glossary
Copyright: Β© 2026 RBXStateMachine contributors Β· Repository Β· License information