WaitFor - easier handling of multiple events

Hey! Thought I’d share a small module I made that helps out with handling many events at once, called WaitFor.


WaitFor.all(…)


After requiring WaitFor, you can use this function to create a signal that only fires once all signals you pass into it are fired. The returned signal will only fire once.

Example;

local GameLoadedAndPlayerAdded = WaitFor.all(game.Loaded, Players.PlayerAdded)

GameLoadedAndPlayerAdded:Connect(print) --> will fire after both game.Loaded and Players.PlayerAdded have fired


WaitFor.any(…)


You can use this function to create a signal that only fires once one of the signals you pass into it are fired. The returned signal will only fire once, and will pass as arguments: the signal you passed in which fired, and any arguments which that signal passed.

Example;

local Part1Changed = workspace.Part1.Changed
local Part2Changed = workspace.Part2.Changed
local Part3Changed = workspace.Part3.Changed

local AnyPartChanged = WaitFor.any(Part1Changed, Part2Changed, Part3Changed)

AnyPartChanged:Connect(function(event, ...) --> will fire when one part changes
    if event == Part1Changed then
        print("Part 1 was changed!")
    elseif event == Part2Changed then
        print("Part 2 was changed!")
    else
        print("Part 3 was changed!")
    end
end)


Try it for yourself


I’ve included a small sample place to show you how the module can be used: WaitFor.rbxl (21.1 KB)

Click the coloured parts to send a signal - the two larger parts light up when their signal is fired.


Get the module


You can find the module on the Roblox library here.

Alternatively, you can download the source code directly: WaitFor.lua (2.2 KB)

Hopefully you guys might find this a bit useful, let me know if there are any issues :slight_smile:

34 Likes

That’s neat, I use something similar in my own utilities. Curious, do you have any clean code examples where you feed a function that takes some parameters in Connect for either of these? It seems like the Connect where you pass the event as parameter easily leads to bad code design (if-else ladders, as per example given). The Connect method I have in my utilities doesn’t do this for this reason.

Out of curiosity, how do the return parameters of WaitFor.all work? Does it just return the contents of the last event that needed to fire, or does it return some kind of array-type structure for all fired?

I would expect it to just return the contents of the last fired event, but the table structure would be useful, especially if it supports more than one return from a subsequent event, maybe something like a dictionary of arrays:

AllEvent = WaitFor.all(event1, event2, event3)

-- what it returns when fired
{
	[event1] = {
		{...}, -- one array for each return, sorted chronologically
		{...}
	},
	[event2] = {
		{...}
	},
	[event3] = {--[[and so on]]}
}
GameLoadedAndPlayerAdded:Connect(function(dict)
	for i, content in ipairs(dict[Players.PlayerAdded]) do
		local player = content[1]
		-- do stuff with player
	end
end)

Edit: It just fires with no arguments, oof.

Edit 2: Modified the WaitFor.all function in the module so it does this, please tell me if you spot any memory leaks:

function WaitFor.all(...)
	local signalTable = {...}
	
	assert(#signalTable > 0, errorMessages.all.noArgs)
	for index, signal in ipairs(signalTable) do
		assert(typeof(signal) == "RBXScriptSignal", errorMessages.all.badArgType:format(index, typeof(signal)))
	end
	
	local allReturn = {}
	
	-- creating a userdata, with a bindable event as a proxy.
	-- this was solely done because bindables cannot return tables with non-alphanumeric keys
	local allSignal = newproxy(true)
	local signalProxy = Instance.new "BindableEvent"
	
	local pseudoConnections = {}
	
	local mt = getmetatable(allSignal)
	
	-- gives access to methods Connect and Wait, like a normal bindable event.
	mt.__index = {
		Connect = function(self, fn)
			local connection = {
				Disconnect = function(self)
					self.Connected = false
				end,
				Connected = true
			}
			
			coroutine.wrap(function()
				signalProxy.Event:Wait()
				while connection.Connected do
					fn(allReturn)
					signalProxy.Event:Wait()
				end
			end)()
			
			pseudoConnections[fn] = connection
			return connection
		end,
		Wait = function(self)
			signalProxy.Event:Wait()
			return allReturn
		end
	}
	
	-- since signals aren't disconnected immediately, a blacklist has to be created
	local fired = {}
	local signalConnections = {}
	
	local pending = #signalTable
	
	for index, signal in ipairs(signalTable) do
		
		-- table for storing all results
		local signalRet = {}
		
		allReturn[signal] = signalRet
		signalConnections[#signalConnections + 1] = signal:Connect(function(...)
			
			signalRet[#signalRet + 1] = {...}
			
			if not fired[signal] then
				fired[signal] = true
				pending = pending - 1
				
				if pending == 0 then
					signalProxy:Fire()
					-- disconnects all events once fired
					for _, c in pairs(signalConnections) do c:Disconnect() end
					for _, c in pairs(pseudoConnections) do c:Disconnect() end
				end
			end
		end)
	end
	
	return allSignal
end

--[[ before
function WaitFor.all(...)
	local signalTable = {...}
	
	assert(#signalTable > 0, errorMessages.all.noArgs)
	for index, signal in ipairs(signalTable) do
		assert(typeof(signal) == "RBXScriptSignal", errorMessages.all.badArgType:format(index, typeof(signal)))
	end
	
	local allSignal = Instance.new "BindableEvent"
	
	local pending = #signalTable
	
	for index, signal in ipairs(signalTable) do
		local connection
		connection = signal:Connect(function()
			connection:Disconnect()
			pending = pending - 1
			
			if pending == 0 then
				allSignal:Fire()
			end
		end)
	end
	
	return allSignal.Event
end
--]]