Skip to content

Instantly share code, notes, and snippets.

@Retinalogic
Created February 26, 2023 21:45
Show Gist options
  • Save Retinalogic/a340af7f9d55523965c806cedbc8825e to your computer and use it in GitHub Desktop.
Save Retinalogic/a340af7f9d55523965c806cedbc8825e to your computer and use it in GitHub Desktop.
Lua-side duplication of the API of events on Roblox objects. Signals are needed for to ensure that for local events objects are passed by reference rather than by value where possible, as the BindableEvent objects always pass signal arguments by value, meaning tables will be deep copied. Roblox's deep copy method parses to a non-lua table compat…
--- Lua-side duplication of the API of events on Roblox objects.
-- Signals are needed for to ensure that for local events objects are passed by
-- reference rather than by value where possible, as the BindableEvent objects
-- always pass signal arguments by value, meaning tables will be deep copied.
-- Roblox's deep copy method parses to a non-lua table compatable format.
-- @classmod Signal
local HttpService = game:GetService("HttpService")
local ENABLE_TRACEBACK = false
local Signal = {}
Signal.__index = Signal
Signal.ClassName = "Signal"
--- Constructs a new signal.
-- @constructor Signal.new()
-- @treturn Signal
function Signal.new()
local self = setmetatable({}, Signal)
self._bindableEvent = Instance.new("BindableEvent")
self._argMap = {}
self._source = ENABLE_TRACEBACK and debug.traceback() or ""
-- Events in Roblox execute in reverse order as they are stored in a linked list and
-- new connections are added at the head. This event will be at the tail of the list to
-- clean up memory.
self._bindableEvent.Event:Connect(function(key)
self._argMap[key] = nil
-- We've been destroyed here and there's nothing left in flight.
-- Let's remove the argmap too.
-- This code may be slower than leaving this table allocated.
if (not self._bindableEvent) and (not next(self._argMap)) then
self._argMap = nil
end
end)
return self
end
--- Fire the event with the given arguments. All handlers will be invoked. Handlers follow
-- Roblox signal conventions.
-- @param ... Variable arguments to pass to handler
-- @treturn nil
function Signal:Fire(...)
if not self._bindableEvent then
warn(("Signal is already destroyed. %s"):format(self._source))
return
end
local args = table.pack(...)
-- TODO: Replace with a less memory/computationally expensive key generation scheme
local key = HttpService:GenerateGUID(false)
self._argMap[key] = args
-- Queues each handler onto the queue.
self._bindableEvent:Fire(key)
end
--- Connect a new handler to the event. Returns a connection object that can be disconnected.
-- @tparam function handler Function handler called with arguments passed when `:Fire(...)` is called
-- @treturn Connection Connection object that can be disconnected
function Signal:Connect(handler)
if not (type(handler) == "function") then
error(("connect(%s)"):format(typeof(handler)), 2)
end
return self._bindableEvent.Event:Connect(function(key)
-- note we could queue multiple events here, but we'll do this just as Roblox events expect
-- to behave.
local args = self._argMap[key]
if args then
handler(table.unpack(args, 1, args.n))
else
error("Missing arg data, probably due to reentrance.")
end
end)
end
--- Wait for fire to be called, and return the arguments it was given.
-- @treturn ... Variable arguments from connection
function Signal:Wait()
local key = self._bindableEvent.Event:Wait()
local args = self._argMap[key]
if args then
return table.unpack(args, 1, args.n)
else
error("Missing arg data, probably due to reentrance.")
return nil
end
end
--- Disconnects all connected events to the signal. Voids the signal as unusable.
-- @treturn nil
function Signal:Destroy()
if self._bindableEvent then
-- This should disconnect all events, but in-flight events should still be
-- executed.
self._bindableEvent:Destroy()
self._bindableEvent = nil
end
-- Do not remove the argmap. It will be cleaned up by the cleanup connection.
setmetatable(self, nil)
end
return Signal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment