Eventer - Make events without external instances

The Title explains is.

This module creates event without using any external objects (instances like binds)

it supports:

  • Connecting multiple functions to an event
  • :Once()
  • Creating unlimited events

When comparing this module to Signal

We get this results:

image

Even though the differents is small, there is still a difference.
Also Eventer is more memory efficient

Both of the events made the same thing

  • Eventer
    image
  • Signal
    image

This module still has a things I would like to optimize. Also I will add some more functionality to it.

I just wanted to share its early version.

Code
local Eventer = {}

local mt = {
	__newindex = function(self,k,v)
		for i,v in self.connections do
			v.func()

			if v.Once then table.remove(self.connections,i) end
		end
	end,
}

function Eventer.new(a)
	
	local t : CustomEvent = {
		connections = {}
	}
		
	function t:Connect(func)
		table.insert(t.connections,{
			func = func,
			Once = false,
		})
		
		return {Disconnect = function() table.remove(t.connections,table.find(t.connections,func)) end}
	end
	
	function t:Once(func)
		table.insert(t.connections,{
			func = func,
			Once = true,
		})
	end
	
	function t:Fire(func)
		t[1] = false
	end
	
	setmetatable(t,mt)
	
	return t
end

return Eventer
2 Likes

I’d just like to add that your code is faster because it doesn’t create, or utilize threads. GoodSignal puts connections in a separate thread, which takes more time.

1 Like

this module is missing a big feature of other signal classes, though: the ability to wait for it to fire

Thats what i said in the title :smile:. It creates event WITHOUT using EXTERNAL instances.

Can you elaborate further? What do you exactly mean by “waiting to fire”? Do you mean Delay?

event:Wait()

it would yield the current thread until the event fires. also, how well does this work when a connection yields?

edit on yielding connections: it doesn’t:
image

however, for simple use cases this is a very nice and easy solution. if you plan on having a simple event in an oop class that will run very often, this would be a good option to consider

1 Like

Yea i havent really implemented it yet. It currently yields when there are multiple functions connected to it (due to its behaviour), i will update it once i either implement a thing im currently working on, or just coroutine them using task.spawn

i think this module does have potential so i wanna provide my signal class that you could use as a reference when you start on these things:

--!strict
export type Connection<A...> = {
	_Callback: (A...) -> (A...),
	_NextConnection: Connection<A...>?,
	_Signal: Signal<A...>,
	_OnlyOnce: boolean,
	__index: Connection<A...>,
	new: (callback: (any) -> (), connectOnce: boolean, signal: Signal<A...>) -> (Connection<A...>),
	Run: (self: Connection<A...>, ...any) -> (),
	Disconnect: (self: Connection<A...>) -> ()


}

export type Signal<A...> = {
	__index: Signal<A...>,
	_Connection: Connection<A...>?,
	Fire: (self: Signal<A...>, A...) -> (),
	Wait: (self: Signal<A...>, duration: number?) -> (A...),
	Connect: (self: Signal<A...>, Callback: (A...) -> ()) -> (Connection<A...>),
	Once: (self: Signal<A...>, Callback: (A...) -> ()) -> (Connection<A...>),
	Destroy: (self: Signal<A...>) -> (),
}


local ConnectionClass: Connection<any> = {} :: Connection<any>
ConnectionClass.__index = ConnectionClass

function ConnectionClass.new(callback, connectOnce, signal)
	return setmetatable({
		_Callback = callback,
		_NextConnection = nil,
		_Signal = signal,
		_OnlyOnce = connectOnce,
	} :: any, ConnectionClass)
end

function ConnectionClass:Run(...)
	task.defer(self._Callback,...)
	if self._OnlyOnce then self:Disconnect() end
end

function ConnectionClass:Disconnect()
	if self._Signal._Connection == self then
		self._Signal._Connection = self._NextConnection
	else
		local priorConnection = self._Signal._Connection
		if priorConnection then
			while priorConnection._NextConnection ~= self do
				priorConnection = priorConnection._NextConnection
			end
			priorConnection._NextConnection = self._NextConnection
		end
	end
end


local SignalClass: Signal<> = {} :: Signal<>



function SignalClass:Fire(...)
	local currentlyRunningConnection = self._Connection
	while currentlyRunningConnection do
		local nextConnection = currentlyRunningConnection._NextConnection
		currentlyRunningConnection:Run(...)
		currentlyRunningConnection = nextConnection

	end
end

function SignalClass:Wait(duration : number?)
	local Running = coroutine.running()

	local _delay
	if duration then
		_delay = task.delay(duration, function(thread)
			task.defer(thread)
		end, Running)
	end

	self:Once(function(...)
		if _delay then task.cancel(_delay) end
		task.defer(Running, ...)
	end)

	return coroutine.yield()
end

function SignalClass:Connect(callback: () -> ())
	local connection = ConnectionClass.new(callback, false, self)
	connection._NextConnection = self._Connection
	self._Connection = connection
	return connection
end

function SignalClass:Once(callback: () -> ())
	local connection = ConnectionClass.new(callback, true, self)
	connection._NextConnection = self._Connection
	self._Connection = connection
	return connection
end

function SignalClass:Destroy()

	self._Connection = nil
	setmetatable(self,nil)

end


return {
	new = function<C...>()
	local self: Signal<C...> = setmetatable({
		_Connection = nil,
	} :: any, {__index = SignalClass})
	return self
	end
}



3 Likes

Just a tip, you can use export type Signal = typeof(SignalClass) so you don’t have to manually the types!

id rather have a type that keeps me in check when writing the class itself but ty

1 Like

i did some optimizations and successfully cut few microseconds
image
im having hard time making the wait functions without it making a big impact on the compilation time

local Eventer = {}

---- simplified funcs ----
local ts = task.spawn
local tw = task.wait
local ti = table.insert
local tc = table.clone
local tr = table.remove
local tf = table.find
---- function ----

local function DoEvent(self,...)	
	for i,v in self.connections do
		ts(v.funcs,...)

		if v.Once == true then table.remove(self.connections,i) end
	end
	
	if self.Fired == true then self.Fired = false end
end

local function Disconnect(self,i)
	tr(self.connections,i)
end
---- general code ----

local mt = {
	__call = DoEvent
}

export type Event<A...> = {
	Fire: (any) -> (nil),
	Wait: () -> (nil),
	Connect: (Callback: () -> (any)) -> (nil),
	Once: (Callback: () -> (any)) -> (nil)
}

local t : Event = {
	connections = {},
	Fired = false
}

function t:Connect(func)
	ti(self.connections,{funcs = func})

	local i = tf(self.connections,func)

	return {Disconnect = function() tr(self,i) end} 
end

function t:Once(func)
	ti(self.connections,{
		funcs = func,
		Once = true
	})
end

function t:Fire(...)
	self(...)
end

function t:Wait()
	self.Fired = true
	repeat tw() until self.Fired == false
end

setmetatable(t,mt)

function Eventer.new()
	local tab = tc(t)
	return 	tab
end

return Eventer

I did some optimization and this is what i got. Any ideas?

this function seems laggy since it repeats task.wait() until the event is fired.

1 Like

this is a good start, but as pointed out it may be laggy.

--!nocheck

local Eventer = {}

---- simplified funcs ----
local ts = task.spawn
local tw = task.wait
local ti = table.insert
local tc = table.clone
local tr = table.remove
local tf = table.find
---- general code ----

export type Event<A...> = {
	Fire: (any) -> (nil),
	Wait: () -> (nil),
	Connect: (Callback: () -> (any)) -> (nil),
	Once: (Callback: () -> (any)) -> (nil)
}

local t : Event = {{}}

function t:Connect(func)
	ti(self[1],func)

	return {Disconnect = function() tr(self[1],tf(self[1],func)) end} 
end

function t:Once(func)
	ti(self[1],-#self[1],func)
end

function t:Fire(...)
	for i,v in self[1] do
		ts(v,...)

		if i < 0 then table.remove(self[1],i) end
	end
end

function t:Wait()
	local Running = coroutine.running()

	local defer = nil

	self:Once(function()
		if defer ~= nil then task.cancel(defer) end
		task.defer(Running)
	end)

	return coroutine.yield()
end

function Eventer.new()
	return 	tc(t)
end

return Eventer

i revamped some things and took your :Wait() to make my own (not really different from yours)

Somehow now it can be compared to GoodSignal (70% of the time Eventer is faster)

2 Likes

that looks much better, good job!

1 Like

@kalabgs! I found how to do this. Because of this wonderful solution by @7z99 Yield a coroutine? - #7 by 7z99 you can make a wait function.

Edit: So sorry, I was so hyped that I didn’t realize you already had an answer!

1 Like

Hey!
Could you edit the original / first post and put an Up-To-Date script?

I’ve really been wanting to do something similar to this module with my own code but I’ve also been stuck thinking about the wait function. I had an idea, what if the connection wait() relied on another API that is versatile enough to provide the built-in wait, like a .Changed:Wait() signal?? (Maybe an instance value that resides underneath the module as a descendant, only created when calling the Wait()

use the task library and coroutine library to yield and resume the thread

1 Like