A type-safe networking library for roblox-ts that simplifies client-server communication.
SignalPacket
: For one-way communication (like RemoteEvents)RequestPacket
: For request-response patterns (like RemoteFunctions)PropertyPacket
: For synchronizing properties between server and clientBatchedPropertyPacket
: For batching property updates over configurable intervalsExactMapPropertyPacket
: For diff-synchronizing values that do not require deep equality checksShallowMapPropertyPacket
: For diff-synchronizing values that require first-level deep equality checksFlamework is required to use Fletchette. This automatically sets up metadata for Fletchette packets.
In tsconfig.json
, add rbxts-transform-flamework
to the plugins
array:
{
"compilerOptions": {
"plugins": [
{ "transform": "rbxts-transform-flamework" }
]
}
}
Then, install Fletchette via npm:
npm install @rbxts/fletchette
import {
signal,
request,
property,
batchedProperty,
primitiveMapProperty,
shallowObjectMapProperty,
smartProperty,
packet,
} from "@rbxts/fletchette";
For one-way communication from server to client or client to server:
// Shared code
const playerJoined = signal<(player: Player, level: number) => void>();
// Server code
playerJoined.toAllClients(newPlayer, 5); // Broadcast to all clients
playerJoined.toClient(specificPlayer, 5); // Send to a specific client
// Client code
playerJoined.fromServer((player, level) => {
print(`${player.Name} joined with level ${level}!`);
});
// Client to server
const requestItem = signal<(itemId: string) => void>();
// Client code
requestItem.toServer("sword123");
// Server code
requestItem.fromClient((player, itemId) => {
print(`${player.Name} requested item: ${itemId}`);
});
For two-way communication (requests that expect a response):
// Shared code
const getPlayerData = request<(userId: number) => PlayerData>();
// Server code
getPlayerData.fromClient((player, userId) => {
// Authenticate and return data
return fetchPlayerData(userId);
});
// Client code
const data = getPlayerData.toServer(123456789);
print(`Got player data: ${data.level}`);
// Server querying client
const getClientInfo = request<() => ClientInfo>();
// Client code
getClientInfo.fromServer(() => {
return {
resolution: workspace.CurrentCamera!.ViewportSize,
performance: stats().Memory,
};
});
// Server code
const clientInfo = getClientInfo.toClient(player);
print(`Client resolution: ${clientInfo.resolution.X}x${clientInfo.resolution.Y}`);
For synchronizing state between server and client:
// Shared code
const playerHealth = property<number>(100);
// Server code
// Update when player takes damage
playerHealth.set(player, 80);
// Client code
playerHealth.observe((newHealth) => {
healthBar.Size = new UDim2(newHealth / 100, 0, 1, 0);
});
While sending the whole value on each update is simple and generally sufficient for 90% of use cases, Fletchette also provides specialized property packets for scenarios that demand more efficient synchronization.
For synchronizing properties while coalescing rapid updates into batches:
// Shared code – batch updates every 100 ms with an initial value of 0
const playerScore = batchedProperty<number>(100, 0);
// Server code
playerScore.set(1250); // Queued and broadcast after the next batch interval
playerScore.setTop(1000); // Only players without overrides receive the new value
// Client code
playerScore.observe((score) => {
scoreLabel.Text = `Score: ${score}`;
});
For synchronizing Map
values that have primitive keys and values, with clients receiving only diff payloads:
// Shared code – primitive map of player stats
const playerStats = primitiveMapProperty<string, number>();
// Server code
playerStats.setEntry("kills", 5); // Sends a single diff entry
playerStats.set(new Map([["deaths", 2]])); // Sends a full replace diff
For synchronizing maps whose values are shallow objects while sending minimal field-level diffs:
// Shared code – map of player loadouts
const playerLoadouts = shallowObjectMapProperty<string, { weapon: string; ammo: number; shield: boolean }>();
// Server code
playerLoadouts.setEntry("player1", { weapon: "Pistol", ammo: 12, shield: false });
playerLoadouts.patchEntry("player1", { ammo: 18 }); // Sends a patch diff with just the ammo change
playerLoadouts.deleteFields("player1", ["shield"]); // Sends a patch diff removing the shield field
playerLoadouts.set(new Map([["player2", { weapon: "Rifle", ammo: 30, shield: true }]])); // Sends a full replace diff
// Client code
playerLoadouts.observe((snapshot, diff) => {
for (const change of diff.changes) {
if (change.type === "replace") {
print(`${change.key} loadout replaced`, change.value);
} else if (change.type === "patch") {
print(`${change.key} loadout patched`, change.sets, change.deletes);
} else {
print(`${change.key} loadout removed`);
}
}
updateLoadoutUI(snapshot);
});
The packet
function automatically chooses the appropriate packet type based on your TypeScript types:
// Creates a SignalPacket (void return type)
const playerJoined = packet<(player: Player, level: number) => void>();
// Creates a RequestPacket (non-void return type)
const getPlayerData = packet<(userId: number) => PlayerData>();
// Creates a PropertyPacket (initial value provided)
const playerHealth = packet<number>({ initialValue: 100 });
// Creates a PropertyPacket (non-function type)
const gameSettings = packet<GameSettings>();
The packet
function supports all the same options as the individual packet creators:
// With unreliable transmission
const fastUpdate = packet<(position: Vector3) => void>({ isUnreliable: true });
// PropertyPacket with initial value and unreliable transmission
const playerPosition = packet({
initialValue: new Vector3(0, 0, 0),
isUnreliable: true
});
// Batched property with a 100 ms flush interval
const leaderboardScore = packet<number>({ initialValue: 0, batchIntervalMs: 100 });
It is not recommended to use packet
for properties that require specialized diffing behavior, such as primitiveMapProperty
or shallowObjectMapProperty
.
Send signals only to players within a certain radius:
// Server code
explosion.toClientsInRadius(bombPosition, 100, explosionStrength);
Send signals to specific groups of players:
// Server code
teamMessage.toClientsInList(teamMembers, "Let's coordinate our attack!");
teamMessage.toClientsExcept(traitor, "There's a traitor among us!");
For detailed API documentation, see the API documentation.
MIT