I was at a friend's place watching a movie when it hit me. Not a slow realization. More like a question that appeared mid-scene and refused to leave: what if signals worked between server and client?
I sat there for the rest of the movie expressionless. My friends were watching. I was somewhere else entirely. A silent room in my head where the only thing that existed was this idea. By the time the credits rolled, I had the entire system designed. Not vaguely. I knew the functions, the data flow, the edge cases. I'd carved the whole thing out in my head while pretending to watch a movie.
The backstory: Sleitnick's Signal library was already everywhere in my codebase. Signals are how modules talk to each other. When something happens, you fire a signal. Other modules listen. Clean, decoupled, simple. But they only work within one environment. A signal on the server can't reach the client. The moment anything needed to cross that boundary, I had to drop signals entirely and wire up a BridgeNet2 bridge instead. A completely different pattern for the same concept. Every time I needed the client to know about something the server did, I had to build dedicated networking for it. An exploding box? That's a bridge. A VFX trigger? Another bridge. A hit confirmation? Another bridge.
By the end of that movie I was already on my laptop reading Sleitnick's source code. And when I read Signal.new, the lightbulb didn't just turn on. It exploded. The function creates a new signal every time. But what if we weren't always making a new signal? What if we could make use of old ones?
Signals are Lua tables with metatables. They can't survive crossing the server/client boundary. When you send a signal to the client, it arrives dead. Just a table with some fields, no metatable, no methods. But the data inside the table survives. So what if every replicated signal carried identification data? A unique ID that the client could use to resurrect it on the other side?
I got a working prototype in two days. Fully integrated into the combat system the next day.
Here's how it works. On the server, when you create a signal and pass a player, it generates a GUID, opens a dynamic BridgeNet2 bridge with that ID, and tells the client about it:
function Signal.new<T...>(plr: Player?): Signal<T...>
local ID = HTTPService:GenerateGUID(false)
local self = setmetatable({
_handlerListHead = false,
_isReplicated = (isServer and plr ~= nil) and plr or nil,
_replicationId = (isServer and plr ~= nil) and ID or nil,
_proxyHandler = nil,
_yieldedThreads = nil,
}, Signal)
if isServer and plr ~= nil then
openReplicatedSignals[ID] = Bridge.ServerBridge(ID)
SignalBridge:Fire(plr, {guid=ID})
end
return self
endThe _replicationId and _isReplicated fields are plain data. A string and a player reference. They survive serialization even when the metatable doesn't.
On the client, Signal.old() takes the dead signal table and brings it back to life:
function Signal.old<T...>(signal): Signal<T...>
if isServer then return end
local repId = rawget(signal, "_replicationId")
local repPlr = rawget(signal, "_isReplicated")
return setmetatable({
_handlerListHead = false,
_isReplicated = repPlr,
_replicationId = repId,
_proxyHandler = nil,
_yieldedThreads = nil,
}, Signal)
endIt reads the GUID from the corpse, creates a fresh signal with the same identity, and gives it back its metatable. The signal is alive again. But it's not connected to anything yet.
The real trick is in Connect. When you connect a handler to a replicated signal on the client, it checks if the bridge was announced and lazily upgrades it from a placeholder to a live connection:
function Signal:Connect(fn)
local connection = setmetatable({
Connected = true,
_signal = self,
_fn = fn,
_next = false,
}, Connection)
if self._handlerListHead then
connection._next = self._handlerListHead
end
self._handlerListHead = connection
if not isServer then
local replicationId = rawget(self, "_replicationId")
if replicationId
and openReplicatedSignals[replicationId] == true then
openReplicatedSignals[replicationId] =
Bridge.ReferenceBridge(replicationId)
openReplicatedSignals[replicationId]._openConnection =
openReplicatedSignals[replicationId]
:Connect(function(...)
self:Fire(...)
end)
end
end
return connection
endThe openReplicatedSignals table starts as true when the client hears the announcement. When Connect sees true, it knows the bridge exists but isn't wired up. It creates the ReferenceBridge, connects, and from that point on, anything the server fires through that signal gets forwarded into the client's local Fire().
And Fire() itself does both: it fires locally for any server-side listeners, and pushes through the bridge to the client:
function Signal:Fire(...)
local item = self._handlerListHead
if isServer
and rawget(self, "_replicationId")
and openReplicatedSignals[self._replicationId] then
openReplicatedSignals[self._replicationId]
:Fire(self._isReplicated, ...)
end
while item do
if item.Connected then
if not freeRunnerThread then
freeRunnerThread =
coroutine.create(runEventHandlerInFreeThread)
end
task.spawn(freeRunnerThread, item._fn, ...)
end
item = item._next
end
endOne :Fire() call. Both sides execute. The server runs damage logic. The client runs VFX. No bridge management, no remote events, no extra wiring.
I started passing signals through every attack in the combat system, regardless of whether the client actually needed them. If the signal exists and the client has a use for it, it hooks in. If not, nothing happens. The system is completely optional from the client's perspective. It just listens if it wants to.
When the signal disconnects or the server is done with it, cleanup happens on both sides:
local function clearReplicatedSignals(plr, ID)
if openReplicatedSignals[ID] == nil then return end
if isServer then
openReplicatedSignals[ID] = nil
SignalBridge:Fire(plr, {guid=ID, kill=true})
else
task.wait()
if openReplicatedSignals[ID]
and openReplicatedSignals[ID]._openConnection then
openReplicatedSignals[ID]._openConnection:Disconnect()
end
openReplicatedSignals[ID] = nil
end
endNo lingering connections. No leaked bridges. The signal lives, does its job, and dies cleanly.
The whole thing took three days. Two for the prototype, one to wire it into the combat system. It started as a question I couldn't stop thinking about during a movie I can barely remember, and ended up changing how every system in the game communicates across the boundary. The rest of the codebase doesn't know the bridge layer exists. Server code creates a signal, fires it. Client code connects, listens. That's it.
Sometimes the best ideas don't come from staring at code. They come from not being able to stop thinking about it, even when you're supposed to be doing something else.