Cleanup
Turning things off. One call drops a carrier and everything it subscribed to, and a dropped carrier goes quiet rather than erroring on you.
Destroy Shared
carrier:Destroy() -> ()
Takes no parameters and returns nothing. Works the same on the server and on the client.
local shop = Pigeon.new("Shop")
shop:On("Buy", function(player, itemId)
print(player.Name, "bought", itemId)
end)
shop:Broadcast("Open", true)
shop:Destroy()
That single call does all of this:
| What | Result |
|---|---|
| Transport bindings | Every subscription the carrier made is disconnected. Nothing arrives any more. |
| Listeners and responders | Cleared. Your On listeners and your When responders are both dropped. |
| Rooms | Cleared. Every room this carrier held is gone. |
| Handshake | The guard is removed and the list of admitted players is emptied. |
| Middleware | Both chains are emptied, so the closures you passed are released. |
| Staged tables | Replication stops. See below for the detail. |
| Engine connections | The PlayerRemoving hook the carrier keeps on the server is disconnected. |
| Sending | The carrier is inert. Nothing it sends leaves the machine. |
| The name cache | The carrier is evicted, so the next Pigeon.new builds a live one. |
| Its transformer | Destroyed, but only if the carrier made it itself. |
Middleware is cleared for a reason worth saying out loud. A middleware function is a closure you wrote, and it can hold a reference to anything at all. Leaving those on a dead object would keep whatever they captured alive.
Calling it twice is safe
The second call does nothing. Teardowns run once and only once, so you never have to guard it or track whether you already did it.
shop:Destroy()
shop:Destroy() -- fine, does nothing
A dropped carrier goes quiet
Sending on a dead carrier does not throw. It just does not happen. Teardown often races with work already in flight, and an error there would spread into code that did nothing wrong.
| Call on a dropped carrier | What happens |
|---|---|
Emit, Broadcast, BroadcastTo, BroadcastExcept | Returns immediately. Nothing sent. |
Call, CallTo | Returns nil straight away. Does not yield. |
Handshake | Returns false. |
Init | Returns the carrier and does nothing else. |
Ping | Returns a number near zero. The round trip never happened. |
On, When | Does nothing. Nothing is registered and your callback is not kept. |
Off | Does nothing. The listener list and the responder slot are already empty. |
Destroy | Does nothing. |
local shop = Pigeon.new("Shop")
shop:Destroy()
shop:Emit("Buy", "sword") -- nothing happens, no error
local answer = shop:Call("Anything") -- nil, returns at once, does not yield
Three calls do not stay silent.
| Call on a dropped carrier | What happens |
|---|---|
SendToRoom | Warns ERR_NO_ROOM and gives up. Destroy cleared the rooms, so the name it is given no longer exists. Nothing is sent. |
RequestTable | Throws ERR_CAPTURE_DENIED. The request underneath returns nil, which reads as a refusal. |
Approved | Returns every player you passed it, or every player in the game if you passed none. Destroy removes the guard, and no guard means no filter. See Known Issues. |
The wrong-machine checks still fire. Calling a server method on the client throws
ERR_NOT_SERVER whether the carrier is alive or not, and the same the
other way round. Being dead does not excuse calling Broadcast from a
LocalScript.
It leaves the name cache
Pigeon.new("Shop") with no options hands back a cached carrier. Destroy
removes that entry, so the next lookup builds a fresh live one instead of handing
you the corpse.
local a = Pigeon.new("Shop")
print(a == Pigeon.new("Shop")) --> true, same object
a:Destroy()
local b = Pigeon.new("Shop")
print(a == b) --> false, b is a new live carrier
Careful
That cache is shared. Every part of your code that asks for "Shop"
holds the same object, so destroying it turns the channel off for all of them,
not just for you. Only destroy a cached carrier when you own the whole channel.
If you want a carrier of your own to destroy, build it with options, which never
gets cached. See Carriers.
Destroying a transformer drops every carrier on it
A transformer is the thing that picks which remote a carrier's traffic rides on. Every carrier that rides on one registers its own teardown there, so destroying the transformer destroys all of them.
local combat = Pigeon.Transformer("Combat")
local damage = Pigeon.new("Damage", { Transformer = combat })
local status = Pigeon.new("Status", { Transformer = combat })
local effects = Pigeon.new("Effects", { Transformer = combat })
-- One call drops all three, with every listener on them.
combat:Destroy()
damage:Broadcast("Hit", 10) -- goes nowhere, the carrier is inert
This is the tidiest way to turn off a whole feature. Put its channels on one transformer when you build them and you have a single switch later. See Transformers.
Ownership decides which way it runs. A carrier that made its own transformer takes
it down with it. A transformer you passed in belongs to you, so
carrier:Destroy() leaves it alone.
| How you built it | carrier:Destroy() | transformer:Destroy() |
|---|---|---|
Pigeon.new("Shop") | Drops the carrier and its transformer | Drops the carrier |
Pigeon.new("Shop", { Transformer = t }) | Drops the carrier, leaves t alone | Drops every carrier on t |
Staged tables survive
A staged table is not owned by the carrier that published it. The carrier is only a pointer to it. Destroying the carrier stops the replication, but the table object and its contents are untouched.
local scores = Pigeon.StagedTable({ round = 1 })
local old = Pigeon.new("Match")
old:CaptureTable("Scores", scores)
old:Destroy()
-- The data is still there, and a new carrier can publish it again.
print(scores:GetTable().round) --> 1
local fresh = Pigeon.new("Match")
fresh:CaptureTable("Scores", scores)
What Destroy does to each side:
| Side | Effect on its staged tables |
|---|---|
| Server | Writes stop being broadcast, and the subscriber list is emptied. The table keeps its data. |
| Client | The local mirror is marked released and stops updating. Its release hook is removed. |
Careful
Destroying a carrier on the client does not tell the server about it. The server
still counts that player as a subscriber until they leave the game. If you want
the server told, call Release() on the mirror before you destroy
the carrier. See Staged Tables.
Turning off one listener instead
Destroy is the big hammer. Most of the time you only want one handler gone, and
that is Off.
Off Shared
carrier:Off(event: string, Callback: ((...any) -> ...any)?) -> ()
| Parameter | Type | What it is |
|---|---|---|
event | string | The event name to unbind. |
Callback | ((...any) -> ...any)? | The one handler to drop, listener or responder. Leave it out to drop every handler for the event. |
Returns nothing.
local shop = Pigeon.new("Shop")
local function onBuy(player, itemId)
print(player.Name, itemId)
end
shop:On("Buy", onBuy)
shop:Off("Buy", onBuy) -- drop this one handler
shop:Off("Buy") -- drop every handler for "Buy"
When the last handler for an event is removed, Pigeon drops the underlying transport
binding too. So Off is real cleanup, not just a filter. The carrier
itself keeps working.
Players who leave clean up on their own
You do not have to do anything when a player disconnects. On the server, Pigeon removes them from every room on the carrier, from the list of players the handshake admitted, and from the subscriber list of every staged table the carrier holds. Anything the server was still holding for that player is thrown away.
A teardown checklist
Tearing down a feature, in order.
Shutting a system down
-
Disconnect anything you connected yourself. Heartbeat loops,
Touchedsignals, tween completions. Pigeon does not know about those. You can hand them totransformer:Trackso they go with the group, which is what Transformer covers. -
On the client, call
Release()on any staged table mirror you no longer want, so the server stops sending you patches. -
Call
Destroy. On the carrier if it is one channel, on the shared transformer if the feature is several channels that belong together. -
Set your references to
nil. The carrier is inert but it is still an object, and something has to let go of it. -
Do not expect
Onto work on it afterwards. It registers nothing. See the note below.
Checking nothing leaked
The transport can tell you what it is still holding on this machine. Useful in tests and when you suspect something is not letting go.
Pigeon.Network.Diagnostics() -> {listeners: number, watchers: number, orphaned: number, pending: number, readySeats: number, bufferedPackets: number, readyChannels: number, bufferedChannels: number, outbound: number}
Takes no parameters. Returns one table of counts.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
local counts = Pigeon.Network.Diagnostics()
print(counts.listeners, counts.pending, counts.bufferedPackets)
| Field | What it counts |
|---|---|
listeners | Event names with at least one transport binding. |
watchers | Channel watchers, added up across every channel. One per live carrier. |
orphaned | Packets held because nothing was listening yet. |
pending | Requests still waiting for an answer. |
readySeats | Player and channel pairs that have called Init. |
readyChannels | Channels with at least one ready client. |
bufferedPackets | Messages queued for clients that are not ready. |
bufferedChannels | Channels with something queued. |
outbound | Packets the client is holding until the remote pool replicates. Always 0 on the server. |
These are counts for the whole machine, not for one carrier. After you tear a system down they should settle back to where they were. Queues and pending requests expire on their own, so give it a few seconds before you decide something is stuck.
On after Destroy
Note
On and When both return straight away on a dropped
carrier. No listener and no responder is registered, nothing is bound underneath,
and the callback you passed is not kept,
so it cannot hold anything alive. That holds whether or not the transformer you
passed in is still alive. There is no way to bring a dropped carrier back, so
build a new one when you need the channel again. See
Known Issues.
What to read next
- Transformers for grouping channels so one call drops them all.
- Known Issues for the sharp edges around teardown.
- Carrier for the full method list.