Lua Signal Class Comparison & Optimal `GoodSignal` Class

Not at this moment, I do not.

I am going off the same assumption as the above

Edit: I’ve found the source of the bug, thank you for your help.

Lately, I’ve thought about how modules that people release on these forums have their own events that feel just like the built-in ones, and have discovered why they use “Signal” and how useful it could be for my own scripts. (Custom events could be so helpful!)

One question, though. I’ve noticed that multiple modules that I’ve added to my game come with their own copies of signal within them. Does having multiple redundant copies of this signal module accessed by different scripts affect performance or is suboptimal in some way?

I’m considering placing the signal module in a universal location then pointing all of the modules to use it instead of their embedded versions, but I don’t know if it’s worthwhile or necessary.

1 Like

Having multiple modules for the same class would only hurt a miniscule amount of performance. I would say plugging in a universal implementation would be useless.


My main problem with any signal implementation, though, is the lack of being able to type-annotate them. I semi-succeeded in creating one that got the datatypes, but there is no way to type the parameter names, making it a bit useless.

For self-serving modules, I usually just use BindableEvents and override their RBXScriptSignal with a pseudo-type. For example:

local Bindable = Instance.new("BindableEvent")

type Callback = (Name: string, Value: number) -> ()
type Signal = {
	Connect: (self: Signal, Callback: Callback) -> RBXScriptConnection,
	Wait: (self: Signal) -> (string, number),
	Once: (self: Signal, Callback: Callback) -> RBXScriptConnection,
	ConnectParallel: (self: Signal, Callback: Callback) -> RBXScriptConnection
}

local Event = (Bindable.Event :: any) :: Signal
Event:Connect(print) -- Callback: (Name: string, Value: number) -> ()

Obviously, this is far from being compact and trivial, but the alternative is no autocomplete.

2 Likes

Not worth the trouble.

Roblox has a parser cache, so if two modules have exactly the same source code in them, the game engine will only parse the module once, and both modules will share most of the same function definitions etc. The only thing that won’t be shared is the closure / table memory which is not very much.

I measured it and extra copy of GoodSignal uses just under 3kb of memory (and no additional network bandwidth because script sources are deduplicated for network replication). 1 full size texture uses 4000kb of memory, so pick your battles. There’s probably better things for you to be spending your time on than deduplicating GoodSignal.

1 Like

Do you ever plan on supporting ConnectParallel? Or is that not possible.

I think it’s too early, with details of parallel Luau still being likely to change a bit. The next major update to parallel Luau is probably the first time I’ll consider it.

1 Like

ConnectParallel can be implemented by adding task.synchronize and task.desynchronize, after that the user need to use Actor Instance for the usage.

Does GoodSignal behave the same as deferred lua event handling as described here?

No, however, if you just replace the line:
task.spawn(freeRunnerThread, item._fn, ...)

With:
task.defer(freeRunnerThread, item._fn, ...)

It will have that behavior. I don’t think it makes sense for me to offer this variant until after the transition to deferred has happened.

1 Like

Hello, I’m still a little confused on the GC behavior of goodsignal, it might be general lua GC behavior, but for some reason cyclic references are causing the GC to not collect things.

Here is an example to show this:

local Weak = require(script.Parent.ModuleScript)
local Table = {
Event = require(game.ServerScriptService.Signal).new()
}

Table.Event:Connect(function()
print(Table)
end)

Weak[2] = Table

Here’s my test for checking if the table has been GCed

local Weak = setmetatable({}, {__mode = ‘v’})

task.spawn(function()
while true do
table.create(10^6)
print(Weak)
task.wait()
end
end)

return Weak

And in this first example, as you see the table never get’s gced.

I know this is specfically an issue with cyclic references, because if I reference an instance it still gets collected

local Weak = require(script.Parent.ModuleScript)
local Table = {
Event = require(game.ServerScriptService.Signal).new()
}

Table.Event:Connect(function()
print(workspace.Baseplate)
end)

Weak[2] = Table

This does get collected.

And the bizzare thing is, this only happens in the script corourtine, if I spawn a new coroutine it does getcollected

local Weak = require(script.Parent.ModuleScript)

task.spawn(function()
local Table = {
Event = require(game.ServerScriptService.Signal).new()
}

Table.Event:Connect(function()
print(Table)
end)

Weak[2] = Table
end)

Which doesn’t make sense to me, because in both cases “Table” it isn’t accessible anymore, once the script is done running or the coroutine finishes. But even more this specfically seems to be an issue with cyclic references

This is a PSA, if you coroutine.yield() inside of a connection callback and then later resume it, DONT RESUME IT MORE THAN ONCE. Seems like common sense but you won’t get any direct errors making it unclear where the problem is and the signal event handler will get messed up. Spent a good hour figuring out why GoodSignal was erroring.

you could probably change the GoodSignal code to check if a RunnerThread was resumed from within GoodSignal and ignore or error otherwise

1 Like

Is this how I would implement a RemoteEvent with a signal?

	local Event = Signal.new(RemoteEvent.OnClientEvent)
	Event:Connect(function()
		print("remote fired to client")
	end)

i think you are looking for this

1 Like

What do I do if I want to delete a signal?

You call :DisconnectAll() to remove all the connections and then remove any references to that signal.

I fire the signal on the server, but nothing is received on the client, how do I fix that?

This is not for use across the server-client boundary, its like a BindableEvent but in luau.

1 Like

You should add my link because it is also used for communication: FastSignal - A consistent signal library

that’s mine @Fast_Duck.

Hey I have a question about the copyright of the goodsignal, it saids it has the “MIT License” which according to the wiki is this:

Copyright (c) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

But this license is for softwares? so I’m not sure where to put the attribution, should I have your name put in every discription of the games I’m making using GoodSignal? What if I’m using it in my plugins? where should I put the credit in that?
Currently I’m making a plugin that sets up a framework with lots of scripts and custom service modules, and I want to use GoodSignal on those modules. Does that change the way I should credit you?