Functions
Functions are BlinkBlox’s version of Roblox’s RemoteFunction. They let one side ask the other a
question and wait for the answer. Unlike a RemoteFunction, a call always ends: it is answered, or it
fails – the listener errored, the call was refused, the player left, or nobody answered within
InvocationTimeout.
Declaring a function
Section titled “Declaring a function”Functions are declared with the function keyword and a block of fields.
function GetBalance { Yield: Coroutine, Data: u8, Return: u32}Yield is required. From, Data, Return, Rate, Burst and Concurrency are optional.
Calls and replies travel on the reliable channel, batched with reliable events and sent once a frame, so a call and its answer each wait for the next flush on their side on top of the network’s own latency.
Default: Client
The side that invokes. The other side listens and answers.
-- The client asks the server. The default, and what every function did before `From` existed.function GetBalance { Yield: Coroutine, Data: u8, Return: u32}
-- The server asks a client, and names which one.function GetClientSetting { From: Server, Yield: Coroutine, Data: u8, Return: string}local Answer = Net.GetClientSetting.Invoke(Player, 1)Net.GetClientSetting.On(function(Which) return Settings[Which]end)A client can only answer a call that was made to it, for the function it was made for: the server keeps each player’s calls apart, so a reply naming a call that belongs to somebody else, or answering one function with another’s return type, is ignored.
How the caller waits for the answer.
Coroutine– the built-in Luau coroutine library.Invokeyields and returns the answer, or throws if the call failed.Future– a future library you provide withoption FutureLibrary. redblox’s Future library is recommended.Invokereturns a future built withFuture.Try, which completes with a success flag followed by the answer.Promise– a promise library you provide withoption PromiseLibrary. evaera’s Promise library, or a fork of it, is recommended.Invokereturns a promise that resolves with the answer or rejects if the call failed. Cancelling it gives its slot back.
A schema that uses Future or Promise without the matching option fails to generate.
The data the caller sends with the call – the client, unless the function is From: Server, in
which case it is the server. Any type, or a
type pack. Omit it when the call carries nothing.
Return
Section titled “Return”The data the listener sends back to the caller – the server, unless the function is
From: Server, in which case it is the client. Any type or type pack, and omitted when the answer
carries nothing.
Data and Return may name their type pack elements the same way – Data: (id: u8, label: string)
alongside Return: (id: u16, label: string) is fine. The generated code keeps the two sets apart, so
there is no need to invent distinct names for the same thing going each way.
function Rename { Yield: Coroutine, Data: (id: u8, label: string(1..32)), Return: (id: u8, label: string(1..32))}Rate, Burst
Section titled “Rate, Burst”Limits how often one player may invoke this function, exactly as on
events. They apply only to a From: Client function – the
default. On a From: Server function they are ignored with a warning, exactly as on a From: Server
event: rate limiting applies to inbound traffic, and the server is the party being protected.
A refused invocation is answered with a failure rather than dropped, so the caller raises promptly instead of waiting out the timeout.
Concurrency
Section titled “Concurrency”How many of one player’s calls to this function the server runs at once. Rate limits how many calls
start each second; it says nothing about how many are still running. A listener that waits – a
DataStore request, an HTTP call, an invocation back to the same client – holds each call open for as
long as the wait, so calls arriving at an allowed rate can still pile up into hundreds of suspended
threads, each holding whatever its listener holds. A client that sends the requests itself is not held
to the thirty-two calls in flight an honest client module is.
function SaveLoadout { Yield: Coroutine, Rate: 2, Concurrency: 1, Data: u8[..8], Return: boolean}With Concurrency: 1, a second SaveLoadout from a player whose first is still saving is refused.
It is answered with a failure straight away, like a call refused by Rate, and reported through the
same rate-limit handler, once a second per player. The place
is given back when the call is answered – whether the listener returned, threw, or returned the wrong
type – and a call waiting in the queue for a listener that has not connected yet counts as running.
When the player leaves, their count goes with them.
Concurrency is a whole number of at least 1. Like Rate, it applies to a From: Client function
and warns (W3020) on a From: Server one.
Usage in Luau
Section titled “Usage in Luau”The module holds one table per function, named after it. The caller’s side gets Invoke, the
listener’s side gets On; the names follow Casing.
Invoking a Function
Section titled “Invoking a Function”local Ok, Balance = pcall(Net.GetBalance.Invoke, 1)local Success, Balance = Net.GetBalance.Invoke(1):Await()Net.GetBalance.Invoke(1) :andThen(function(Balance) -- ... end) :catch(warn)On the server, Invoke of a From: Server function takes the player to ask first:
Net.GetClientSetting.Invoke(Player, 1).
Listening to a Function
Section titled “Listening to a Function”Net.GetBalance.On(function(Player, Account) return Balances[Player][Account]end)The listener receives the calling Player first on the server; on a client, answering a
From: Server function, it receives only the data. Each call runs on a thread of its own, so the
listener may yield – a datastore read, say – and whatever it returns is the answer.
A function has one listener. On returns nothing, and calling it again replaces the listener.
Calls that arrive before a listener connects are queued, up to 256 on the server, and run when it does.
Past that the server answers each further call with a failure, so the caller’s Invoke raises
promptly instead of hanging. A client’s queue is filled by the server, and warns once it passes 256
instead.
When a call fails
Section titled “When a call fails”Every failure reaches the caller the same way: a Coroutine invoke throws
There was an exception while processing "Name"., a Future completes with false, and a Promise
rejects. What causes it:
| Cause | What else happens |
|---|---|
| The listener threw | The listening side warns "Name" encountered an error, ... with the error. |
The listener returned something its Return type does not allow |
The same warning. The reply is taken back out of the batch rather than sent half-written. |
The call was refused by Rate |
Reported through the rate-limit handler. |
The player already had Concurrency calls running |
Reported through the same handler, with the function’s name. |
| The server’s queue for an unconnected listener was full | Nothing is reported. |
Nobody answered within InvocationTimeout |
The caller’s side warns, naming the function and, on the server, the player. |
| The player being asked left the game | Every call outstanding to them fails at once rather than timing out. |
A late answer, arriving after its call has already failed, is ignored.
Some calls fail before anything is sent, and throw from Invoke itself (or reject, or complete with
false, for the other yield types):
- The arguments do not serialise – see
WriteValidations. - The server invokes a player who is no longer in the game.
- Thirty-two calls are already in flight.
Thirty-two calls in flight
Section titled “Thirty-two calls in flight”A call is known by a one-byte id that its answer carries back, and at most 32 calls may be
outstanding at once – per client module for calls a client makes, and per player for calls the
server makes, so asking every player a question at once does not exhaust anything. A thirty-third
concurrent call throws 32 calls are already awaiting a response, this call has been dropped.
A slot is given back when its call is answered, fails, times out or – for a promise – is cancelled,
so reaching the cap takes thirty-two calls genuinely stuck at once. Without the timeout they would be
stuck forever; with it, the worst case is
InvocationTimeout seconds, ten by default.
