Subscription class

Simple module that is based on Roblox’s events, but uses ‘keys’ instead of classes, supports timeouts in the :Await method and you can pass arguments to the callback from inside the :Subscribe method, making it easier to use a single function for multiple subscriptions

Demo:

local Event = require(game.ReplicatedStorage.Event)

local TestEvent = Event.new()

local function Test(...)
    print(...)
end

TestEvent:Subscribe("TestKey", Test, "Subscription One")
TestEvent:Subscribe("TestKey2", Test, "Subscription Two")

TestEvent:Fire("Fire")

delay(3, function() TestEvent:Fire() end)

print("Waiting")
TestEvent:Await()
print("Waited")

print("Waiting again")
TestEvent:Await(2)
print("Timed out after 2 seconds")

TestEvent:Unsubscribe("TestKey2")

TestEvent:Fire()

--If you dont care what the key is, pass nil and the method will return the key attached to the subscription
local Key = TestEvent:Subscribe(nil, Test, "No key given to this one")

TestEvent:Fire()

TestEvent:Unsubscribe(Key)

TestEvent:Fire()

Event.lua (1.8 KB)

Lua source
local Class = {}
Class.__index = Class

function Class.new()
	return setmetatable({}, Class)
end

function Class:Fire(...)
	if (self.Subscriptions) then
		for _, Subscription in pairs(self.Subscriptions) do
			if (Subscription.Arguments) then
				coroutine.wrap(Subscription.Callback)(unpack(Subscription.Arguments), ...)
			else
				coroutine.wrap(Subscription.Callback)(...)
			end
		end
	end
	
	if (self.Threads) then
		for Key = #self.Threads, 1, -1 do
			local Thread = self.Threads[Key]
			if (Thread) then
				table.remove(self.Threads, Key)
				coroutine.resume(Thread, ...)
			end
		end
		
		if (not next(self.Threads)) then
			self.Threads = nil
		end
	end
end

function Class:Subscribe(Key, Callback, ...)
	if (not self.Subscriptions) then
		self.Subscriptions = {}
	end
	
	if (not Key) then
		if (not self.UID) then
			self.UID = 0
		end
		
		Key = tostring(self.UID)
		self.UID = self.UID + 1
	end
	
	self.Subscriptions[Key] = {Key = Key, Callback = Callback, Arguments = (... and {...} or nil)}
	
	return Key, self.Subscriptions[Key]
end

function Class:Unsubscribe(Key)
	if (not self.Subscriptions or not self.Subscriptions[Key]) then return end
	
	self.Subscriptions[Key] = nil
	
	if (not next(self.Subscriptions)) then
		self.Subscriptions = nil
	end
end

function Class:Await(Timeout)
	if (not self.Threads) then
		self.Threads = {}
	end
	
	local Thread = coroutine.running()
	table.insert(self.Threads, Thread)
	
	if (Timeout) then
		delay(Timeout, function()
			if (not Thread) then return end
			
			local Index = (self.Threads and table.find(self.Threads, Thread))
			if (Index) then
				table.remove(self.Threads, Index)
			end
			
			coroutine.resume(Thread)
		end)
	end
	
	coroutine.yield()
	
	Thread = nil
end

return Class
9 Likes

What are the advantages of using this as opposed to ROBLOX events?

2 Likes

A few of the biggest advantages I can think of off the top of my head, just from reviewing the source code:

  • No Instance overhead if that ever bothered you,

  • (Slightly) simplified API,

    • Event:Await() instead of Bindable.Event:Wait()
    • unsubbing from the event object itself instead of a separate connection object
  • More flexibility over how the event behaves,

    • timeout feature on :Await
    • extra arguments supplied on :Subscribe
    • ability to redefine callbacks and arguments dynamically from the second result returned from Event:Subscribe
    • Unsubbing all (or specific) subscriptions by looping through Event.Subscriptions
    • Resuming all awaiting threads by looping through Event.Threads
2 Likes