Overview
What it is
Wispr
Server-authoritative state replication system for Roblox TypeScript
Wispr provides a simple, safe, and explicit way to replicate state from server to clients using a snapshot + patch model. Built for roblox-ts.
Need help or found a bug? Join the Discord and ask:
Features
- Server Authoritative: Only the server can mutate state, ensuring security and consistency
- Type Safe: Full TypeScript support with type inference
- Efficient: Snapshot + patch model minimizes bandwidth usage
- Scoped Replication: Control which clients see which state nodes
- Versioned Patches: Automatic handling of out-of-order patches
- Observable: Listen to changes at specific paths or any change in a node
Installation
npm install @rbxts/wispr
Quick Start
Server Setup
import { WisprToken, createNode, patchNode, pathOf } from "@rbxts/wispr";
// Define a token for your state type
interface PlayerStats {
gold: number;
level: number;
inventory: string[];
}
const PLAYER_STATS = WisprToken.create<PlayerStats>("player.stats");
// Create a node (server-only)
const node = createNode(
PLAYER_STATS,
{ kind: "all" }, // Scope: replicate to all clients
{ gold: 0, level: 1, inventory: [] } // Initial state
);
// Update state with patches
patchNode(PLAYER_STATS, { type: "set", path: pathOf("gold"), value: 100 });
patchNode(PLAYER_STATS, { type: "increment", path: pathOf("level"), delta: 1 });
Client Setup
import { waitForNode, requestInitialData, pathOf } from "@rbxts/wispr";
// Initialize (call once on client startup)
await requestInitialData();
// Wait for and get the node
const node = await waitForNode(PLAYER_STATS);
const stats = node.getState();
// Listen for changes
node.listenForChange(pathOf("gold"), (newVal, oldVal) => {
print(`Gold changed from ${oldVal} to ${newVal}`);
});
// Or listen for any change
node.listenForAnyChange(() => {
print("Stats updated!");
});
// Listen for nodes matching a pattern (similar to ReplicaService)
import { onNodeOfClassCreated, requestInitialData } from "@rbxts/wispr";
await requestInitialData();
// Listen for any player data node (e.g., "player.data.123", "player.data.456")
onNodeOfClassCreated("player.data.", (node) => {
print(`Player data node created: ${node.token.id}`);
const data = node.getState();
// Setup UI, listeners, etc. for this player's data
node.listenForChange(pathOf("coins"), (newCoins) => {
updateCoinsUI(newCoins as number);
});
});
Core Concepts
Tokens
Tokens are unique identifiers for state nodes. Create them once at module level:
const PLAYER_STATS = WisprToken.create<PlayerStats>("player.stats");
const GAME_STATE = WisprToken.create<GameState>("game.state");
Paths
Paths are arrays of keys (never strings) that navigate the state tree:
pathOf("inventory", "items", "sword_001") // ["inventory", "items", "sword_001"]
pathOf("weapons", 0, "cooldown") // ["weapons", 0, "cooldown"]
Scopes
Control which clients receive state updates:
{ kind: "all" } // All clients
{ kind: "player", player: somePlayer } // Single player
{ kind: "players", players: [p1, p2] } // Multiple players
Patch Operations
Wispr supports various patch operations:
// Set a value
{ type: "set", path: pathOf("gold"), value: 100 }
// Delete a value
{ type: "delete", path: pathOf("oldField") }
// Increment a number
{ type: "increment", path: pathOf("level"), delta: 1 }
// Array operations
{ type: "listPush", path: pathOf("inventory"), value: "sword" }
{ type: "listInsert", path: pathOf("inventory"), index: 0, value: "potion" }
{ type: "listRemoveAt", path: pathOf("inventory"), index: 2 }
// Map operations
{ type: "mapSet", pathToMap: pathOf("players"), id: "player123", value: playerData }
{ type: "mapDelete", pathToMap: pathOf("players"), id: "player123" }
Helper Functions
Wispr provides helper functions for creating patch operations:
import { opSet, opIncrement, opListPush, opMapSet } from "@rbxts/wispr";
patchNode(PLAYER_STATS, opSet(pathOf("gold"), 100));
patchNode(PLAYER_STATS, opIncrement(pathOf("level"), 1));
patchNode(PLAYER_STATS, opListPush(pathOf("inventory"), "sword"));
patchNode(PLAYER_STATS, opMapSet(pathOf("equipment"), "weapon", swordData));
// Apply multiple operations at once
patchNodeMultiple(PLAYER_STATS, [
opSet(pathOf("gold"), 200),
opIncrement(pathOf("level"), 1),
]);
API Reference
Server API
// Create a new state node
createNode<T>(token: WisprToken<T>, scope: WisprScope, initialState: T): WisprServerNode<T>
// Get a node by token (server-side)
getServerNode<T>(token: WisprToken<T>): WisprServerNode<T> | undefined
// Destroy a node
destroyNode(token: WisprToken<unknown>): void
// Apply a single patch operation
patchNode(token: WisprToken<unknown>, operation: WisprPatchOp): void
// Apply multiple patch operations
patchNodeMultiple(token: WisprToken<unknown>, operations: readonly WisprPatchOp[]): void
Client API
// Request initial data (call once on startup)
requestInitialData(): Promise<void>
// Wait for a node to be created
waitForNode<T>(token: WisprToken<T>): Promise<WisprNode<T>>
// Get a node (returns undefined if not found)
getClientNode<T>(token: WisprToken<T>): WisprNode<T> | undefined
// Listen for nodes matching a token ID pattern
onNodeOfClassCreated(pattern: string, callback: (node: WisprNode) => void): () => void
WisprNode (Client)
[README truncated — view the full file on GitHub]
Install
npm install @rbxts/wispr