Startup and Buffering
A game starts sending before every client is ready to listen. Pigeon holds those
messages instead of losing them, and lets them go when the client says it is ready.
That is all Init does.
The problem
Server scripts and local scripts do not start at the same moment. The server can load, decide the shop stock changed and broadcast it, all before a joining client's LocalScript has run a single line.
With a plain RemoteEvent that message is gone. It fired, nothing was connected, and nobody will ever hear about it. You end up writing a "tell me the current state" round trip for every system just to paper over the first second of the session.
Init Client
carrier:Init() -> PigeonCarrier
The client registers its handlers first, then calls Init once.
Init tells the server "I am ready on this channel", and the server
sends everything it has been holding for that client on that channel, oldest first.
It takes no parameters and it never yields.
Returns the same carrier, so you can chain from it.
local shop = Pigeon.new("Shop")
shop:On("StockChanged", updateShopUi)
shop:On("Sold", playSoldSound)
-- Handlers are in place. Let the backlog through.
shop:Init()
A message that arrives too early
Once a client is ready on a channel it stays ready for the rest of its session. Later broadcasts go straight out with no queue involved.
The rules of Init
| Rule | What it means for you |
|---|---|
| Client only. | On the server it does nothing at all. It does not error, so shared code can call it without checking which machine it is on. |
| It returns the carrier. | You can chain it, or ignore the return value. |
| Calling it twice is safe. | The second call does nothing. There is no second flush and no warning. |
| It does not yield. | It returns immediately, even on the first frame, before the remotes have replicated in. If there is no remote to send on yet the readiness packet is held and goes out ahead of everything else the carrier queued behind it. See The Ref Pool. |
| It is per channel. | Readying "Shop" releases the "Shop" backlog and nothing else. |
| It only opens the inbound direction. | The client can Emit and Call from its first frame with no Init at all. |
| A dropped carrier ignores it. | No error. See Cleanup. |
Chaining works because Init hands the carrier back:
local shop = Pigeon.new("Shop")
shop:On("StockChanged", updateShopUi)
local same = shop:Init()
print(same == shop) --> true
Readiness is per channel
The queue is kept per client, per channel, and a channel is just a carrier's name. Bringing one channel up says nothing about any other.
local shop = Pigeon.new("Shop")
local quests = Pigeon.new("Quests")
shop:On("StockChanged", updateShopUi)
shop:Init() -- Shop's backlog arrives now
-- Quests is still queued on the server. Nothing has been lost.
task.wait(5)
quests:On("Updated", updateQuestUi)
quests:Init() -- Quests' backlog arrives now
This is why a system that loads late does not lose its opening messages just because another system was quicker.
Note
Readiness is keyed by the channel name, not by the carrier object. If you build
two carriers with the same name on one client, calling Init on
either one opens that name for both of them.
What the queue will and will not hold
| Limit | Behaviour |
|---|---|
| 64 messages | Per client, per channel. When a 65th arrives the oldest one is dropped to make room. |
| 30 seconds | A held message older than this is swept away, whether the queue is full or not. |
| The player leaves | Everything held for them is dropped at once, and nothing can queue for them again. |
| Unreliable sends | Never queued. A message that was allowed to go missing in transit has no business turning up a minute late, so it is simply dropped if the client is not ready. See Unreliable Sending. |
| Replies to a request | Never queued. The far side is already yielded on the answer, so holding it would strand that thread until its timeout. |
| Server to client requests | Queued like any other message. A CallTo to a client that has not called Init waits in the queue, and the server thread waits with it. Unless that client opens the channel inside the call's timeout, the call comes back nil. See Requests and Replies. |
Careful
A client that never calls Init hears nothing from the server on that
channel. Its own Emit and Call keep working perfectly,
which makes this a quiet bug: half the channel works. If a broadcast is not
showing up, check that Init was called on that exact channel name.
Orphan replay: the last few frames
Init covers the gap before the client is ready. There is a second,
smaller gap it cannot cover: a message that arrives on a ready channel a moment
before the handler for that particular event exists.
Pigeon holds those too. A message with no listener for its event is kept for 10 seconds and replayed to the first listener that registers for it.
A message that arrives before its handler
Worth knowing about it:
- It works on both machines. A client
Emitthat beats the server'sOnis held the same way. - It is per channel and per event name, and holds up to 64 of them. The oldest goes first when that fills.
- The held messages are replayed once, to the first listener only. A second listener registered afterwards does not see them.
- 10 seconds, then they are swept.
Treat this as a safety net, not a plan. It exists because the moment a client declares itself ready the server starts sending, and that can land a frame or two before your game code has finished wiring up.
The rule
Register every On you care about, then call Init once.
That is the whole thing.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Pigeon = require(ReplicatedStorage.Pigeon)
local shop = Pigeon.new("Shop")
-- 1. every handler first
shop:On("StockChanged", updateShopUi)
shop:On("Sold", playSoldSound)
shop:On("Restocked", flashRestockBadge)
-- 2. then open the channel, once
shop:Init()
-- 3. now use it normally
print(shop:Call("Buy", "sword"))
The server has nothing to do. It never calls Init, and it does not need
to know which clients are ready. It just sends, and Pigeon decides whether that goes
out now or waits.
What to read next
- Sending and Receiving for the messages being queued in the first place.
- Requests and Replies for the one kind of traffic that never waits.
- Carrier API for every other method on a carrier.