Types
Pigeon exports five Luau types. You only need them if you run strict mode or if you pass Pigeon objects between your own functions and want them annotated. This page shows each definition as it is written and how to use it.
Using them
The types hang off the module you already required. Luau keeps types and values in
separate namespaces, so the same Pigeon name works for both.
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
local shop: Pigeon.PigeonCarrier = Pigeon.new("Shop")
Careful
Do not reach through Pigeon.Types. That field is the boolean
true at runtime, because the types file holds only type declarations
and returns true. Write Pigeon.PigeonCarrier, not
Pigeon.Types.PigeonCarrier.
| Type | What it describes |
|---|---|
Pigeon.Transformer | A transformer. |
Pigeon.StageTable | A staged table. |
Pigeon.PigeonCarrier | A carrier. |
Pigeon.MiddlewareFunction | A middleware function. |
Pigeon.StageGuard | A capture or release guard. |
The types
Transformer
export type Transformer = {
uuid: string,
destroyed: boolean,
_teardown: {() -> ()},
Track: (self: Transformer, disconnect: () -> ()) -> (() -> ()),
Destroy: (self: Transformer) -> ()
}
The object Pigeon.Transformer
returns. uuid is the string that gets hashed to pick a remote, and
destroyed flips to true once and stays there.
local combat: Pigeon.Transformer = Pigeon.Transformer("Combat")
local connection = workspace.ChildAdded:Connect(onPartAdded)
local untrack: () -> () = combat:Track(function()
connection:Disconnect()
end)
_teardown is the list of functions Track collects. It is in
the type but it is not for you. Use Track and the handle it hands back.
See Transformer.
StageTable
export type StageTable = {
GetTable: (self: StageTable) -> {[any]: any},
UseCapture: (self: StageTable, Callback: (...any) -> ()) -> (),
UseRelease: (self: StageTable, Callback: (...any) -> ()) -> (),
Release: (self: StageTable) -> ()
}
The object Pigeon.StagedTable
returns. Note the names differ by one letter: the function is
StagedTable, the type is StageTable.
local state: Pigeon.StageTable = Pigeon.StagedTable({ round = 1 })
local view: {[any]: any} = state:GetTable()
view.round = 2
GetTable is typed as a plain {[any]: any}, so the checker
will let you write anything into it. What you write is still checked at runtime, and
a value that cannot travel as data errors on the spot. See
StagedTable.
PigeonCarrier
export type PigeonCarrier = {
Unreliable: boolean,
--Server
BroadcastTo: (self: PigeonCarrier, Players: {Player}, event: string, ...any) -> (),
Broadcast: (self: PigeonCarrier, event: string, ...any) -> (),
BroadcastExcept: (self: PigeonCarrier, Players: {Player}, event: string, ...any) -> (),
CallTo: (self: PigeonCarrier, Target: Player, Event: string, ...any) -> ...any,
SendToRoom: (self: PigeonCarrier, Room: string, event: string, ...any) -> (),
CreateRoom: (self: PigeonCarrier, RoomID: string, Players: {Player}) -> (),
DestroyRoom: (self: PigeonCarrier, RoomID: string) -> (),
JoinRoom: (self: PigeonCarrier, RoomID: string, Players: {Player} | Player) -> (),
LeaveRoom: (self: PigeonCarrier, RoomID: string, Players: {Player} | Player) -> (),
CaptureTable: (self: PigeonCarrier, id: string, Table: StageTable) -> (),
ReleaseTable: (self: PigeonCarrier, id: string) -> (),
ForceTable: (self: PigeonCarrier, id: string, Table: StageTable, Targets: ({Player} | Player)?) -> (),
UseHandshake: (self: PigeonCarrier, Callback: (player: Player, ...any) -> boolean) -> (),
Approved: (self: PigeonCarrier, Targets: {Player}?) -> {Player},
Revoke: (self: PigeonCarrier, Targets: {Player} | Player) -> number,
--Client
Emit: (self: PigeonCarrier, event: string, ...any) -> (),
Call: (self: PigeonCarrier, event: string, ...any) -> ...any,
RequestTable: (self: PigeonCarrier, id: string) -> StageTable,
Ping: (self: PigeonCarrier) -> number,
Handshake: (self: PigeonCarrier, ...any) -> boolean,
Init: (self: PigeonCarrier) -> PigeonCarrier,
--Shared
On: (self: PigeonCarrier, event: string, Callback: (...any) -> ()) -> (),
When: (self: PigeonCarrier, event: string, Callback: ((...any) -> ...any)?) -> (),
Off: (self: PigeonCarrier, event: string, Callback: ((...any) -> ...any)?) -> (),
UseIncoming: (self: PigeonCarrier, Callback: (...any) -> ()) -> (),
UseOutgoing: (self: PigeonCarrier, Callback: (...any) -> ()) -> (),
Destroy: (self: PigeonCarrier) -> ()
}
The object Pigeon.new returns. Every
method takes self first, so you call them all with a colon.
local shop: Pigeon.PigeonCarrier = Pigeon.new("Shop")
local function announce(carrier: Pigeon.PigeonCarrier, stock: {any})
carrier:Broadcast("StockChanged", stock)
end
announce(shop, getStock())
One type covers both machines. The comments in the definition say which side each
method belongs to. A server method called on the client errors with
ERR_NOT_SERVER, and a client method called on the server errors with
ERR_NOT_CLIENT. Two of them do not follow that rule:
Approved has no side check at all, and Init is a no-op on
the server so shared code can call it either way. The checker will not stop you, so
the comments are the only warning you get. See Carrier.
On and When are the two ways to handle an event, and the type
says which is which. An On callback returns (), because a
listener's return value has nowhere to go. A When callback returns
...any, because a responder's return values are the reply to a
Call or a CallTo. Both callbacks are optional in
When and Off: passing nil to When
clears the responder for that event.
Either way the arguments differ by machine. A server handler is handed the sending player first, a client handler is not.
local shop = Pigeon.new("Shop")
shop:When("Buy", function(player: Player, itemId: string)
return giveItem(player, itemId)
end)
local shop = Pigeon.new("Shop")
shop:On("StockChanged", function(stock: {any})
updateShopUi(stock)
end)
shop:Init()
MiddlewareFunction
export type MiddlewareFunction = (direction: string, event: string, args: {any}, player: Player?) -> (string?, {any}?)
The shape of a function you hand to UseIncoming or
UseOutgoing. Middleware sees every packet the carrier handles and can
rewrite it before it goes on.
| Parameter | Type | What it is |
|---|---|---|
direction | string | Either "incoming" or "outgoing". |
event | string | The event name as you wrote it, such as "Buy". |
args | {any} | The arguments as an array. |
player | Player? | The other machine's player, on the server only. |
event is the carrier local name, not the name that travels. The channel
prefix is added after outgoing middleware has run and is already gone by the time
incoming middleware runs, so you always see the plain name.
Both returns are optional, and each is used only if it is the right type. Return a string to rename the event. Return a table to replace the arguments. Return nothing and the packet passes through untouched.
Renaming does a different job in each direction. Outgoing, the new name is what travels, so the other machine has to listen for that name. Incoming, the new name picks which of your listeners runs, and the carrier hands middleware every packet on its channel, so the original name does not need a listener of its own.
local shop = Pigeon.new("Shop")
local log: Pigeon.MiddlewareFunction = function(direction, event, args, player)
print(direction, event, #args, player)
return nil, nil
end
-- Written as "Buy", it arrives on the far side as "Purchase".
local rename: Pigeon.MiddlewareFunction = function(_direction, event, args)
if event == "Buy" then
return "Purchase", args
end
return nil, nil
end
shop:UseIncoming(log)
shop:UseOutgoing(rename)
Note
player is not filled in as often as you might expect on the way out.
Incoming on the server it is the player who sent the packet. Outgoing on the
server two paths fill it in: CallTo, with the player being asked,
and losing access, with the player being cleared out on the reserved
__pigeon_table_revoke message it sends them. That second one covers
Revoke and a handshake the guard turned down, since both evict the
player the same way. The broadcast methods leave it nil. On the
client it is always nil.
Middleware runs in the order you added it, and each one is handed what the previous one produced. If yours errors, Pigeon warns and moves on to the next with the packet unchanged. See Middleware.
StageGuard
export type StageGuard = (player: Player, id: string) -> boolean?
The shape of a function you hand to UseCapture or
UseRelease on a staged table. It runs on the server when a client asks
for that table or asks to stop receiving it.
| Parameter | Type | What it is |
|---|---|---|
player | Player | The client asking. |
id | string | The id the table was captured under. |
Return false to block the attempt. Anything else allows it, including
nil and returning nothing at all, so a guard that forgets to return is a
guard that lets everyone through.
local ownerOnly: Pigeon.StageGuard = function(player, id)
return id == "profile_" .. player.UserId
end
local profiles = Pigeon.new("Profiles")
local function publish(owner: Player)
local profile = Pigeon.StagedTable({ coins = 0 })
profile:UseCapture(ownerOnly)
profiles:CaptureTable("profile_" .. owner.UserId, profile)
end
Guards run in the order added and every one has to pass. The first false
ends it. If a guard errors, the attempt is blocked, so a broken guard fails shut
rather than open.
Capture guards only judge requests a client made. ForceTable is the
server deciding, so it does not ask them. See
Staged Tables.
What the list leaves out
These types describe the public surface, not the whole object. Some things that exist at runtime are not in them.
| Missing from | What |
|---|---|
PigeonCarrier |
Timeout, which is a real field set from the options table. |
StageTable |
Snapshot, ApplySnapshot, ApplyPatches, CanCapture and CanRelease, which Pigeon uses itself. |
| All three objects | The private fields, the ones starting with an underscore. |
Transformer._teardown is the exception. It is in the type, but it is
still internal.
Note
The callback parameters on UseIncoming, UseOutgoing,
UseCapture and UseRelease are all typed loosely as
(...any) -> () rather than as
MiddlewareFunction and StageGuard. Annotate your
function where you write it, the way the samples above do, and the checker still
has something to work with.
Everything in the types is real. Nothing listed is missing at runtime, so if the checker accepts a call, that method exists.