· 4 years ago · Sep 26, 2021, 07:10 PM
1--- Lua-side duplication of the API of events on Roblox objects.
2-- Signals are needed for to ensure that for local events objects are passed by
3-- reference rather than by value where possible, as the BindableEvent objects
4-- always pass signal arguments by value, meaning tables will be deep copied.
5-- Roblox's deep copy method parses to a non-lua table compatable format.
6-- @classmod Signal
7
8local HttpService = game:GetService("HttpService")
9
10local ENABLE_TRACEBACK = false
11
12local Signal = {}
13Signal.__index = Signal
14Signal.ClassName = "Signal"
15
16--- Constructs a new signal.
17-- @constructor Signal.new()
18-- @treturn Signal
19function Signal.new()
20 local self = setmetatable({}, Signal)
21
22 self._bindableEvent = Instance.new("BindableEvent")
23 self._argMap = {}
24 self._source = ENABLE_TRACEBACK and debug.traceback() or ""
25
26 -- Events in Roblox execute in reverse order as they are stored in a linked list and
27 -- new connections are added at the head. This event will be at the tail of the list to
28 -- clean up memory.
29 self._bindableEvent.Event:Connect(function(key)
30 self._argMap[key] = nil
31
32 -- We've been destroyed here and there's nothing left in flight.
33 -- Let's remove the argmap too.
34 -- This code may be slower than leaving this table allocated.
35 if (not self._bindableEvent) and (not next(self._argMap)) then
36 self._argMap = nil
37 end
38 end)
39
40 return self
41end
42
43--- Fire the event with the given arguments. All handlers will be invoked. Handlers follow
44-- Roblox signal conventions.
45-- @param ... Variable arguments to pass to handler
46-- @treturn nil
47function Signal:Fire(...)
48 if not self._bindableEvent then
49 warn(("Signal is already destroyed. %s"):format(self._source))
50 return
51 end
52
53 local args = table.pack(...)
54
55 -- TODO: Replace with a less memory/computationally expensive key generation scheme
56 local key = HttpService:GenerateGUID(false)
57 self._argMap[key] = args
58
59 -- Queues each handler onto the queue.
60 self._bindableEvent:Fire(key)
61end
62
63--- Connect a new handler to the event. Returns a connection object that can be disconnected.
64-- @tparam function handler Function handler called with arguments passed when `:Fire(...)` is called
65-- @treturn Connection Connection object that can be disconnected
66function Signal:Connect(handler)
67 if not (type(handler) == "function") then
68 error(("connect(%s)"):format(typeof(handler)), 2)
69 end
70
71 return self._bindableEvent.Event:Connect(function(key)
72 -- note we could queue multiple events here, but we'll do this just as Roblox events expect
73 -- to behave.
74
75 local args = self._argMap[key]
76 if args then
77 handler(table.unpack(args, 1, args.n))
78 else
79 error("Missing arg data, probably due to reentrance.")
80 end
81 end)
82end
83
84--- Wait for fire to be called, and return the arguments it was given.
85-- @treturn ... Variable arguments from connection
86function Signal:Wait()
87 local key = self._bindableEvent.Event:Wait()
88 local args = self._argMap[key]
89 if args then
90 return table.unpack(args, 1, args.n)
91 else
92 error("Missing arg data, probably due to reentrance.")
93 return nil
94 end
95end
96
97--- Disconnects all connected events to the signal. Voids the signal as unusable.
98-- @treturn nil
99function Signal:Destroy()
100 if self._bindableEvent then
101 -- This should disconnect all events, but in-flight events should still be
102 -- executed.
103
104 self._bindableEvent:Destroy()
105 self._bindableEvent = nil
106 end
107
108 -- Do not remove the argmap. It will be cleaned up by the cleanup connection.
109
110 setmetatable(self, nil)
111end
112
113return Signal