Why don’t you call coroutine.wrap()
?
Instead of:
function obj:Fire(...)
if disable then return error("Cannot use Fire after Destroy function call.") end
for i=1, #callList do callList[i][1](...) end
for i=1, #waitList do coroutine.resume(waitList[i], ...) end
end
Why not just:
function obj:Fire(...)
if disable then return error("Cannot use Fire after Destroy function call.") end
for i=1, #callList do coroutine.wrap(function() callList[i][1](...) end)() end
end
After the callList[i][1](...)
call is done, you can compare the number of completed listeners to the total amount of listeners, and if they are equal, you can resume all the threads that are waiting on the event.
Tables should be copied for fires. This will prevent uncommon but hard to solve bugs when you modify the listener table while it is being iterated over. You can optimize this by only deep copying the listener table if it has been changed since the last deep copy, otherwise use the last deep copy. This should be efficient enough in many cases.
I must confess I do not see the use in destroying an event object. When is this a useful operation? This just seems like it’s going to cause bugs.
As for BindableFunction
s, there’s really no reason to add them at all, because they predate ModuleScript
s, which have superceded them completely.
I’ve written a BindableEvent
-like replacement myself before a number of months ago, the difference is that in my implementation the fire call yields until all listeners have been completed (listeners can make asynchronous calls, this will not delay the total runtime of the fire unnecessarily):
local message_broadcaster = {}
local services = {
replicated_storage = game:GetService("ReplicatedStorage"),
}
local core_import_root = services.replicated_storage.core
local imports = {
core = {
table_utility = require(core_import_root.table_utility),
},
}
local channel_data = {}
local function create_channel(name)
local new_data_of_channel = {
listener_amount = 0,
listeners = {},
}
channel_data[name] = new_data_of_channel
return new_data_of_channel
end
local function destroy_channel(name)
channel_data[name] = nil
end
function message_broadcaster.subscribe(channel_name, listener)
local data_of_named_channel = channel_data[channel_name]
if not data_of_named_channel then
data_of_named_channel = create_channel(channel_name)
end
data_of_named_channel.listeners[listener] = true
data_of_named_channel.listener_amount = data_of_named_channel.listener_amount + 1
end
function message_broadcaster.send(channel_name, ...)
local data_of_named_channel = channel_data[channel_name]
if not data_of_named_channel then
return
end
local packed_message = {...}
local listeners_copy = imports.core.table_utility.get_deep_copy(data_of_named_channel.listeners)
local listener_amount_copy = data_of_named_channel.listener_amount
local completion_event = Instance.new("BindableEvent")
local completed_listener_amount = 0
local is_broadcast_completed = false
local function handle_broadcast_completion()
is_broadcast_completed = true
end
completion_event.Event:Connect(handle_broadcast_completion)
for current_listener in pairs(listeners_copy) do
local function current_listener_wrapper()
current_listener(unpack(packed_message))
completed_listener_amount = completed_listener_amount + 1
if completed_listener_amount == listener_amount_copy then
completion_event:Fire()
end
end
local current_listener_thread = coroutine.wrap(current_listener_wrapper)
current_listener_thread()
end
if not is_broadcast_completed then
completion_event.Event:Wait()
end
completion_event:Destroy()
end
function message_broadcaster.unsubscribe(channel_name, listener)
local data_of_named_channel = channel_data[channel_name]
data_of_named_channel.listeners[listener] = nil
data_of_named_channel.listener_amount = data_of_named_channel.listener_amount - 1
if data_of_named_channel.listener_amount > 0 then
return
end
destroy_channel(channel_name)
end
return message_broadcaster
I think it’s much simpler just to have a simple pub-sub API like mine, regardless of the implementation details, rather than introducing OOP without any real benefit. I can’t see a gain in your approach vs. just sending messages on a channel specified by a string name.
table_utility module relevant to my code:
local table_utility = {}
local function get_deep_copy(table_to_copy, previously_copied_tables)
local copy_of_table = {}
if not previously_copied_tables then
previously_copied_tables = {}
end
local previous_copy_of_table = previously_copied_tables[table_to_copy]
if previous_copy_of_table then
return previous_copy_of_table
end
previously_copied_tables[table_to_copy] = copy_of_table
local metatable_to_copy = getmetatable(table_to_copy)
if metatable_to_copy then
local copy_of_metatable = nil
local previous_copy_of_metatable = previously_copied_tables[metatable_to_copy]
if previous_copy_of_metatable then
copy_of_metatable = previous_copy_of_metatable
else
copy_of_metatable = get_deep_copy(metatable_to_copy, previously_copied_tables)
end
setmetatable(copy_of_table, copy_of_metatable)
end
for current_key, current_value in pairs(table_to_copy) do
if type(current_key) == "table" then
current_key = get_deep_copy(current_key, previously_copied_tables)
end
if type(current_value) == "table" then
current_value = get_deep_copy(current_value, previously_copied_tables)
end
copy_of_table[current_key] = current_value
end
return copy_of_table
end
function table_utility.get_deep_copy(table_to_copy)
return get_deep_copy(table_to_copy)
end
return table_utility