Pigeon

The module you get back from require. It has four functions and two fields. Most games only ever call the first function.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
MemberWhat it is
Pigeon.newLooks up or builds a carrier.
Pigeon.StagedTableBuilds a staged table.
Pigeon.TransformerBuilds a transformer.
Pigeon.GetRefResolves the RemoteEvent a transformer rides on.
Pigeon.NetworkThe low level transport.
Pigeon.TypesThe types module, which is just true at runtime.

The same module runs on the server and on the client. Nothing here is one sided. It is the carrier methods that split, and those are on the Carrier page.

Functions

Pigeon.new Shared

Pigeon.new(name: string, options: {Unreliable: boolean?, Timeout: number?, Transformer: Transformer?}?) -> PigeonCarrier

Returns the carrier for a channel name. The name is the channel and the only thing both machines have to agree on. Build Pigeon.new("Shop") on the server and on the client and those two are talking.

ParameterTypeWhat it is
namestringThe channel name. Must not be empty.
optionstable?Transport settings. Passing one at all skips the cache.

Returns a carrier. See Carrier for what you can do with it.

Server
local shop = Pigeon.new("Shop")

shop:When("Buy", function(player, itemId)
	return giveItem(player, itemId)
end)
Client
local shop = Pigeon.new("Shop")

shop:On("StockChanged", updateShopUi)
shop:Init()

print(shop:Call("Buy", "sword"))

The caching rule

There are two behaviours in one function. The options table is the switch.

Pigeon.new

just a name look in the cache the same carrier every time
name and options skip the cache a brand new carrier every time
local a = Pigeon.new("Shop")
local b = Pigeon.new("Shop")
print(a == b) --> true

local c = Pigeon.new("Shop", { Timeout = 3 })
local d = Pigeon.new("Shop", { Timeout = 3 })
print(c == d) --> false

Options describe one particular carrier. If Pigeon handed back a cached one your settings would be thrown away without a word, so it builds a fresh carrier instead.

Careful

The only check is whether options is nil. Even Pigeon.new("Shop", {}) skips the cache, because an empty table is still not nil. Pass nothing when you want the shared carrier.

The cache never hands back a dead carrier. A cached carrier removes itself when it is dropped, whether that was carrier:Destroy() or its transformer being destroyed, so the next call builds a live replacement. A carrier that comes back already dropped is not cached at all.

local shop = Pigeon.new("Shop")
shop:Destroy()

print(Pigeon.new("Shop") == shop) --> false

The options table

FieldTypeDefaultWhat it does
Unreliable boolean? false Sends fire and forget messages over the lossy lane.
Timeout number? 10 seconds How long a request waits for an answer. Call, CallTo, Ping, Handshake and RequestTable all read it.
Transformer Transformer? one built from name The transformer this carrier rides on.

Unreliable has to be exactly true. Anything else, including a truthy value like 1, leaves the carrier reliable. See Unreliable Sending.

Timeout is stored on the carrier and left as nil when you do not pass one, and the transport falls back to 10 seconds at send time. Both Unreliable and Timeout stay on the carrier as plain fields, so you can change them later.

local chat = Pigeon.new("Chat")

chat.Timeout = 3
chat.Unreliable = true

Transformer puts several named channels on one remote and lets you drop them all in one call. Leave it out and the carrier builds its own transformer using the channel name as the uuid. See Transformers.

Errors

ErrorWhen
ERR_NO_NAME name is not a string, or is the empty string.
ERR_NO_TRANSFORMER options.Transformer was given but is not a table with a string uuid.
Pigeon.new("")                                  -- ERR_NO_NAME
Pigeon.new("Shop", { Transformer = "Combat" })  -- ERR_NO_TRANSFORMER

Both are raised inside Pigeon, one level up from the carrier constructor, so the traceback lands on Pigeon's own line rather than on the call that made the carrier. The error name is what tells you which of the two went wrong.

Warning

Because options build a fresh carrier every time, you can end up with two carriers on one name. Incoming messages are routed by name alone, so the two share the channel: a packet goes to every carrier that bound its event with On or When, and every listener they registered for it runs. A carrier that did not bind that event is skipped, its middleware included. Keep the receiving side to a single carrier per name. See Known Issues.

Pigeon.StagedTable Shared

Pigeon.StagedTable(initial: {[any]: any}?) -> StageTable

Builds a staged table: a plain table whose writes replicate to the clients holding it. You write to it on the server and every client that has it sees the change.

ParameterTypeWhat it is
initial{[any]: any}?Starting contents. Deep copied in, not held by reference.

Returns a staged table. See StagedTable.

Server
local state = Pigeon.StagedTable({ round = 1, scores = {} })

local carrier = Pigeon.new("Match")
carrier:CaptureTable("match", state)

local t = state:GetTable()
t.round = 2
t.scores.Alice = 10

Making one does not publish it. Nothing replicates until a server carrier captures it with CaptureTable or pushes it out with ForceTable. Clients get theirs from RequestTable rather than by building one.

Careful

initial is checked as it is copied, and a bad value errors right there rather than later on the wire. Staged tables carry plain Roblox data only.

ErrorWhat you put in
ERR_STAGE_UNSUPPORTEDA type that cannot travel as data, such as an Instance.
ERR_STAGE_BEHAVIOURA function or a thread.
ERR_STAGE_METATABLEA table with a metatable, which includes class instances.
ERR_STAGE_CYCLEA table that refers back into itself.
ERR_STAGE_TABLE_KEYA table used as a key.

Each message names the path that broke it, like root.stats.health, so you do not have to hunt for the offending field.

Note

The function is called StagedTable but the Luau type is called StageTable. That is not a typo on this page. See Types.

Pigeon.Transformer Shared

Pigeon.Transformer(uuid: string?) -> Transformer

Builds a transformer. A transformer decides which pooled RemoteEvent traffic rides on, and it is the switch that turns a group of carriers off.

ParameterTypeWhat it is
uuidstring?The identity to adopt. Left out, you get a fresh GUID.

Returns a transformer. See Transformer.

local combat = Pigeon.Transformer("Combat")

local damage = Pigeon.new("Damage", { Transformer = combat })
local status = Pigeon.new("Status", { Transformer = combat })

-- One call drops both channels and every listener on them.
combat:Destroy()

The uuid is what gets hashed to pick a remote, so pass the same string on both machines when both of them build one. A generated GUID is fine when the transformer only exists to group things on one side.

Note

A carrier with no Transformer option builds one using its channel name as the uuid. So Pigeon.Transformer("Shop") hashes to the same remote as a plain Pigeon.new("Shop") carrier. Sharing a remote is harmless. A carrier only hears events sent on its own channel name.

Building one has a side effect: it counts toward the number of transformers Pigeon knows about, which is what the remote pool is sized from. On the server it also creates the remote for that bucket straight away and republishes the pool size so clients can find it. Destroying the transformer takes the count back down. See The Ref Pool.

Pigeon.GetRef Shared Yields

Pigeon.GetRef(transformer: Transformer) -> RemoteEvent

Resolves the pooled RemoteEvent a transformer's traffic goes out on. Carriers do this for you, so you rarely need it. It is handy when you want to see which remote a channel actually landed on.

ParameterTypeWhat it is
transformerTransformerThe transformer to resolve.

Returns the reliable RemoteEvent serving that transformer.

local combat = Pigeon.Transformer("Combat")
local remote = Pigeon.GetRef(combat)

print(remote.Name)          --> PigeonRef_1
print(remote.Parent.Name)   --> PigeonRefs

The remotes live in a folder called PigeonRefs under ReplicatedStorage. Each bucket has a reliable PigeonRef_N and an UnreliableRemoteEvent twin called PigeonRefU_N. This function only gives you the reliable one.

Careful

On the client this yields until the remote has replicated from the server. On the server it creates the remote if it is not there yet, so it returns straight away.

Do not hold on to what you get back. The pool is one remote per 16 transformers, at least 1 and at most 32, so the pool grows and shrinks as transformers come and go, and the same uuid can move to a different bucket when it does. Ask again each time.

It also does not care whether the transformer was destroyed. A destroyed transformer still hashes to a bucket, so you still get a remote back.

Fields

Pigeon.Network Shared

Pigeon.Network: Network

The low level transport that carriers are built on. It owns the remote pool, the buffering, the call timeouts and the raw send functions. You do not need it for normal work, but it is the place to look when you want numbers.

print(Pigeon.Network.KnownTransformers)  -- how many transformers exist here
print(Pigeon.Network.GetRefCount())      -- how many remotes the pool holds

local counts = Pigeon.Network.Diagnostics()
print(counts.pending, counts.bufferedPackets, counts.outbound)

Full list on the Network page.

Pigeon.Types Shared

Pigeon.Types: boolean

The types module. It holds nothing but export type declarations, and the file returns true, so this field is the boolean true at runtime. There is nothing in it to read or call.

print(Pigeon.Types)         --> true
print(typeof(Pigeon.Types)) --> boolean

Careful

You cannot write Pigeon.Types.PigeonCarrier as a type annotation. The types are re-exported on the module itself, so write Pigeon.PigeonCarrier.

Exported types

Five types are exported from the module for strict mode. They are re-exports of the ones in the types module.

local shop: Pigeon.PigeonCarrier = Pigeon.new("Shop")
local combat: Pigeon.Transformer = Pigeon.Transformer("Combat")
local state: Pigeon.StageTable = Pigeon.StagedTable()
TypeWhat it describes
Pigeon.PigeonCarrierA carrier.
Pigeon.TransformerA transformer.
Pigeon.StageTableA staged table.
Pigeon.MiddlewareFunctionAn incoming or outgoing middleware function.
Pigeon.StageGuardA capture or release guard on a staged table.

Pigeon.Transformer is both a function and a type. Luau keeps values and types apart, so the same name works in both places and nothing clashes.

See Types for the full definitions.