This announcement was very nonchalantly made, and has been met with a large amount of concern from many developers including myself:
Not only does this lead to a very large amount of undefined/unpredictable behavior in existing code, there’s even a number of bugs with existing core script code (see: Camera panning is one-frame-off from input when Workspace.SignalBehavior is set to Deferred)
This update also, in effect, makes it so I have to replace 100% of instances in my code when I use BindableEvent objects, since the behavior of events in general has fundamentally changed. This requires me to update a large number of public libraries and old codebases in order to conform with this change. For a lot of these instances, I have already switched from using plain old BindableEvent instances, to using a custom Signal class:
Workarounds I've made so far to future-proof my code:
Signal (ModuleScript):
--!strict
local FastSpawnListener = Instance.new('BindableFunction')
FastSpawnListener.OnInvoke = function(cb: (...any) -> (), ...: any)
cb(...)
end :: any
export type Connection = {
Connected: boolean,
Disconnect: () -> (),
Destroy: () -> (),
}
export type FunctionArgs<T> = any --[[typeof((function(...)
local x: T
x(...)
return ...
end)())]]
export type Signal<T> = {
Connect: (Signal<T>, T) -> Connection,
Wait: (Signal<T>) -> ...FunctionArgs<T>,
Fire: (Signal<T>, ...FunctionArgs<T>) -> (),
Destroy: () -> (),
}
local Connection = {}
Connection.__index = {}
function Connection.__index.Disconnect(self: any)
if self.Connected then
self.Connected = false
self._signal._cbSet[self._cb] = nil
self._signal = nil
self._cb = nil
end
end
Connection.__index.Destroy = Connection.__index.Disconnect
function Connection.new(_signal: any, _cb: any): Connection
local self = {}
setmetatable(self, Connection)
self.Connected = true
self._signal = _signal
self._cb = _cb
return self :: any
end
local Signal = {}
Signal.__index = {}
function Signal.__index.Connect(self: any, cb: any): Connection
local conn = Connection.new(self, cb)
self._cbSet[cb] = true
return conn
end
function Signal.__index.Destroy(self: any)
self._cbSet = {}
self._threads = {}
end
function Signal.__index.Wait(self: any): ...any
local thread = coroutine.running()
table.insert(self._threads, thread)
return coroutine.yield()
end
function Signal.__index.Fire(self: any, ...: any)
local cbs = {}
for cb in pairs(self._cbSet) do
table.insert(cbs, cb)
end
for i = 1, #cbs do
local cb = cbs[i]
coroutine.resume(
coroutine.create(
function(...)
FastSpawnListener:Invoke(cb, ...)
end
),
...
)
end
local threads = self._threads
if #threads > 0 then
self._threads = {}
for i = 1, #threads do
coroutine.resume(threads[i], ...)
end
end
end
function Signal.new(): Signal<any>
local self = {}
setmetatable(self, Signal)
self._cbSet = {}
self._threads = {}
return self :: any
end
return Signal
FastSpawn (ModuleScript):
--!strict
local FAST_SPAWN_BINDABLE = Instance.new('BindableFunction')
local FAST_SPAWN_CALLER = function(cb: () -> ())
cb()
end :: any
local LOCK_IS_INVOKING = false
FAST_SPAWN_BINDABLE.OnInvoke = FAST_SPAWN_CALLER
local function FastSpawn(func: () -> ())
if LOCK_IS_INVOKING then
local nestedCallFastSpawnBindable = Instance.new('BindableFunction')
nestedCallFastSpawnBindable.OnInvoke = FAST_SPAWN_CALLER
coroutine.resume(
coroutine.create(function()
nestedCallFastSpawnBindable:Invoke(func)
end)
)
else
LOCK_IS_INVOKING = true
coroutine.resume(
coroutine.create(function()
FAST_SPAWN_BINDABLE:Invoke(func)
end)
)
LOCK_IS_INVOKING = false
end
end
return FastSpawn
Because Workspace.SignalBehavior is not accessible by scripts, the time is still ripe to make an API change that would save a lot of headache from the vast majority of roblox developers with a large, existing codebase that relies on the immediate behavior of signals firing.
My proposal is that there should be a boolean property on Workspace, akin to StreamingEnabled (which also makes very impactful, breaking changes to an existing codebase), called SignalDeferenceEnabled
. In the future, when parallel Luau is released, having this property set to true could be a requirement to enabling parallel luau.
What this means is that existing games don’t have to be updated at all, since when migrating an old file, SignalDeferenceEnabled
can be set to false. However, for newly-created places, this can be set to true by default, since these places can start fresh with new, deferred-signal-compatible code.