Skip to content

Replication

-- Server
Replication.SetFor(player, "Wallet", { Coins = 250 })
-- Client
Replication.Subscribe("Wallet.Coins", function(coins)
coinLabel.Text = tostring(coins)
end, bag)

Twill.Replication resolves to a different module on each side. The server gets the publisher; the client gets the local view.

The publisher lives outside ReplicatedStorage, so its source never reaches a player. A client cannot ask the server for state and cannot write any. Reads on the client answer from what has already arrived, so they never yield and answer nil for anything that has not.

Where a client has a real question, Net answers it with a remote that replies.

A key set with Set reaches every player. A key set with SetFor reaches one.

Both can use the same name. A player who has a value of their own under a key sees that value; everyone else sees the shared one. This is how a scoreboard and a private balance can share a key name without the private one leaking.

Anything after the first dot is a path inside the key, so a key name cannot contain a dot.

Writes are collected and sent on an interval rather than per write. Setting one key several times in a frame costs one message.

Each key travels as its own call. A key too large to send, or holding a value Types.Any cannot carry, holds back only itself: it is reported, the record of what the player last received stays where it was, and it goes again the next time the key is written. Every other key still arrives.

What is sent is a difference, not the whole value. Each player has a private record of what they were last sent, and only the fields that moved travel. A first message carries everything the player can see.

Three controls sit on top of that:

ControlEffect
SetThrottleHolds a key to at most one message per interval. Nothing is lost, only delayed.
FreezeStops sending a key while it is rearranged. Writes still land.
UnfreezeResumes, and everything the key missed goes out together.

SetValidator refuses writes to a key unless a test passes. One guard per key.

Every way of writing a key answers to it: Set and SetFor, and the path forms SetPath, SetPathFor, Increment and IncrementFor. A path write is tried on a copy first, so a refusal leaves nothing behind.

Clearing a key is always allowed. A guard that raises counts as a refusal and is reported.

[Server]

Publishes a value every player can see.

function Replication.Set(key: string, value: any)

Nothing is sent at once. Pass nil to clear the key.

Throws when the key is empty.

[Server]

Writes one field inside a shared key, building the tables along the way.

function Replication.SetPath(key: string, path: string, value: any): boolean

Returns

boolean - false when a step along the path is blocked by something that is not a table, or when the guard refused.

[Server]

Adds to a number inside a shared key, starting from zero when there is none.

function Replication.Increment(key: string, path: string, amount: number): number?

Returns

number? - The new value, or nil when the field holds something that is not a number, or the write was refused.

[Server]

Replaces a shared key with whatever the given function returns.

function Replication.Mutate<T>(key: string, transform: (current: T?) -> T)

[Server]

Publishes a value only one player receives.

function Replication.SetFor(player: Player, key: string, value: any)

Does nothing for a player who is not on this server. Works from the moment the player joins, including inside a PlayerAdded listener that runs before this module has heard of the join.

[Server]

Writes one field inside a player’s own key.

function Replication.SetPathFor(player: Player, key: string, path: string, value: any): boolean

Returns

boolean - false when the player is not here, the path is blocked, or the guard refused.

[Server]

Adds to a number inside a player’s own key.

function Replication.IncrementFor(player: Player, key: string, path: string, amount: number): number?

[Server]

Replaces one player’s own key with whatever the given function returns.

function Replication.MutateFor<T>(player: Player, key: string, transform: (current: T?) -> T)

[Server]

Returns what one player sees under a key.

function Replication.GetFor<T>(player: Player, full: string): T?

The published value itself, not a copy. Treat it as read only.

[Server]

Refuses writes to a key unless the given test passes.

function Replication.SetValidator(key: string, validator: ((value: any) -> boolean)?)

Pass nil to remove the guard.

[Server]

Holds one key to at most one message per interval.

function Replication.SetThrottle(key: string, interval: number?)

Pass nil to remove the limit.

[Server]

Stops sending a key while it is rearranged.

function Replication.Freeze(key: string)

[Server]

Resumes a key that was held back.

function Replication.Unfreeze(key: string)

[Server]

Returns a signal fired whenever a key is written.

function Replication.OnChanged(key: string): Signal<(value: any, player: Player?) -> ()>

Fires on every write, which is not the same as every change. The second argument is the owning player for a player key, and nil for a shared one.

[Server]

Returns how much has gone out since this server started.

function Replication.GetStats(): { Messages: number, Keys: number }

A snapshot, safe to keep. It does not follow later counting. Each key travels as its own call, so both fields count key updates sent.

[Server] | [Client]

Returns what this side holds for a key, or one field inside it.

function Replication.Get<T>(full: string): T?

The held value itself, not a copy. On the client this reads the local view and never asks the server.

[Client]

Calls back whenever a key, or one field inside it, moves.

function Replication.Subscribe<T>(full: string, callback: (value: T?) -> (), owner: Bag?): Subscription

Parameters

NameTypeDescription
fullstringA key, optionally followed by a dot separated path.
callback(value: T?) -> ()Receives the new value, or nil when it is cleared.
ownerBag?A bag to put the subscription in. The caller keeps it when left out.

Returns

Subscription - Carries Disconnect and Destroy, so a bag holds it like anything else.

The current value arrives once at the moment of subscribing, if there already is one. A subscription to a path is woken only when that path actually moved, not whenever anything in the key did.

Throws when the key is empty or no callback is given.

[Client]

Yields until a key, or a field inside it, holds a value.

function Replication.WaitFor<T>(full: string, timeout: number?): (T?, boolean)

Returns

T? - The value, or nil when the wait ran out.

boolean - false when the wait ran out. Yields.

Giving up is reported through the second return, never raised.

LimitValue
Time between messages0.1 seconds
WaitFor default timeout10 seconds
Wait for the server to register30 seconds
Requests for a full view2 per second, per player
Largest value one key carriesThe server to client bounds of Types.Any
Reports of a key that could not be sent1 per 5 seconds, per player and key