docs: update readme with new charm-sync details

This commit is contained in:
richard 2026-06-21 18:40:03 -07:00 • committed by GitHub
parent ba870a8c2c
commit fdb506dc23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

125
README.md
View file

@ -623,34 +623,43 @@ CharmSync = "littensy/charm-sync@VERSION"
### Quick Start ### Quick Start
Start by specifying the signals that the server should sync to clients. For this example, we'll use the first and last name signals: Start by specifying the signals that the server should sync to clients. For this example, we'll replicate a round status and timer signal, and each player's inventory:
```luau ```luau
-- nameStore -- store.luau
local getName, setName = signal("John")
local getSurname, setSurname = signal("Doe") local getStatus, setStatus = signal("intermission")
local ageAtom = atom(20) local timerAtom = atom(20)
local getInventories, setInventories = signal({} :: { [Username]: Inventory })
return { return {
getName = getName, getStatus = getStatus,
setName = setName, setStatus = setStatus,
getSurname = getSurname, timerAtom = timerAtom,
setSurname = setSurname, getInventories = getInventories,
ageAtom = ageAtom, setInventories = setInventories,
} }
``` ```
#### Server setup
When a player notifies the server that they're ready to start syncing, call `server.addSignalsToClient` with the signals that the client should receive updates for. Once they leave, call `server.removeClient` to unsubscribe them from all updates. When a player notifies the server that they're ready to start syncing, call `server.addSignalsToClient` with the signals that the client should receive updates for. Once they leave, call `server.removeClient` to unsubscribe them from all updates.
Then, use `server.connect` to specify how state updates should be sent to each client. Pass a callback function that fires a remote with the given target player and the state updates they subscribed to: Then, use `server.connect` to specify how state updates should be sent to each client. Pass a callback function that fires a remote with the given target player and the state updates they subscribed to:
```luau ```luau
-- sync.server.luau
playerReadyEvent.OnServerEvent:Connect(function(player) playerReadyEvent.OnServerEvent:Connect(function(player)
-- Sync signal getters, computed signals, atoms, or reactive proxies -- Sync signal getters, computed signals, atoms, or reactive proxies
server.addSignalsToClient(player, { server.addSignalsToClient(player, {
name = nameStore.getName, status = store.getStatus,
surname = nameStore.getSurname, timer = store.timerAtom,
age = nameStore.ageAtom, -- Signals unique to a player should also have a key unique to
-- that player, to avoid overwriting other synced signals
[`inventory-{player.UserId}`] = computed(function()
return store.getInventories()[player.Name]
end),
}) })
end) end)
@ -659,30 +668,51 @@ Players.PlayerRemoving:Connect(function(player)
end) end)
server.connect(function(player, updates) server.connect(function(player, updates)
-- Customize how you send state updates to clients -- Customize how you replicate state updates to clients
syncEvent:FireClient(player, updates) syncEvent:FireClient(player, updates)
end) end)
-- Changes now get replicated to clients
store.setStatus("started")
store.timerAtom(30)
``` ```
> [!NOTE] > [!WARNING]
> On the server, make sure each key corresponds to the same data across all players. If two players subscribe to the same key, but were given different signals, Charm will output a warning. > On the server, make sure each key corresponds to the same signal across all players. In other words, if you're syncing a signal that is unique to a player, ensure that the key is also unique to that player.
>
> If two players subscribe to the same key, but were assigned different signals for that key, Charm will output a warning.
#### Client setup
To sync the client with the server's state, call `client.addSignals` with a table of writable signals (setter functions or atoms) whose keys match their server counterparts. To sync the client with the server's state, call `client.addSignals` with a table of writable signals (setter functions or atoms) whose keys match their server counterparts.
After the client receives updates from the server, call `client.patch` to patch the client's signals with the incoming state updates. After the client receives updates from the server, call `client.patch` to patch the client's signals with the incoming state updates.
```luau ```luau
-- sync.client.luau
-- The server is replicating a signal containing the client's
-- inventory, so this signal will receive those updates
local getInventory, setInventory = signal({} :: Inventory)
-- Add signal setters, atoms, or reactive proxies -- Add signal setters, atoms, or reactive proxies
client.addSignals({ client.addSignals({
name = nameStore.setName, status = store.setStatus,
surname = nameStore.setSurname, timer = store.timerAtom,
age = nameStore.ageAtom, -- Signals unique to a player should also have a key unique to
-- that player, to avoid overwriting other synced signals
[`inventory-{Players.LocalPlayer.UserId}`] = setInventory,
}) })
-- Update client signals when receiving updates from the server -- Update client signals when receiving updates from the server
syncEvent.OnClientEvent:Connect(function(updates) syncEvent.OnClientEvent:Connect(function(updates)
client.patch(updates) client.patch(updates)
end) end)
-- The signals will now update to match the server's state
subscribe(getInventory, function(inventory)
print("My inventory is", table.concat(inventory))
end)
``` ```
--- ---
@ -702,38 +732,42 @@ A configuration table that customizes the behavior of Charm Sync on the server.
### `server.addSignalsToClient(client, getters)` ### `server.addSignalsToClient(client, getters)`
The `addSignalsToClient` function subscribes a client to updates in the given signals. When an update occurs, the client will receive a state patch of only the values that changed. The `addSignalsToClient` function replicates changes in the given signals to a specific client. When an update occurs, the client will receive a delta-compressed state patch containing only the values that changed.
You can pass signal getter functions, computed signals, atoms, and [reactive objects](#reactiveinitialvalue) in the `getters` table. This function can also be called multiple times on the same client to subscribe to new signals. You can pass signal getter functions, computed signals, atoms, and [reactive proxies](#reactiveinitialvalue) in the `getters` table. This function can also be called multiple times on the same client to subscribe to new signals.
```luau ```luau
playerReadyEvent.OnServerEvent:Connect(function(player) playerReadyEvent.OnServerEvent:Connect(function(player)
server.addSignalsToClient(player, { server.addSignalsToClient(player, {
name = nameStore.getName, status = store.getStatus,
surname = nameStore.getSurname, timer = store.timerAtom,
age = nameStore.ageAtom,
}) })
end) end)
``` ```
You're also allowed to create new signals to sync to specific players, as long as the key is unique to that data: You're also allowed to create new signals to sync to specific players, as long as the key is unique to that signal:
```luau ```luau
playerReadyEvent.OnServerEvent:Connect(function(player) playerReadyEvent.OnServerEvent:Connect(function(player)
server.addSignalsToClient(player, { server.addSignalsToClient(player, {
-- 🟢 Good: Player-specific data is synced with a unique key -- 🟢 Good: Player-specific signals are synced with a unique key
[`data-{player.UserId}`] = computed(function() [`playerData-{player.UserId}`] = computed(function()
return getPlayerData(player.UserId) return clientStore.getPlayerData(player.UserId)
end), end),
-- 🔴 Bad: Syncing different data with the same keys does not work -- 🔴 Bad: Syncing different signals with the same keys does not work
playerData = computed(function() playerData = computed(function()
return getPlayerData(player.UserId) return clientStore.getPlayerData(player.UserId)
end), end),
}) })
end) end)
``` ```
> [!NOTE]
> On the server, make sure each key corresponds to the same signal across all players. In other words, if you're syncing a signal that is unique to a player, ensure that the key is also unique to that player.
>
> If two players subscribe to the same key, but were assigned different signals for that key, Charm will output a warning.
--- ---
### `server.removeSignalsFromClient(client, ...keys)` ### `server.removeSignalsFromClient(client, ...keys)`
@ -742,11 +776,11 @@ The `removeSignalsFromClient` function unsubscribes the client from a list of ke
```luau ```luau
server.addSignalsToClient(player, { server.addSignalsToClient(player, {
name = nameStore.getName, status = store.getStatus,
surname = nameStore.getSurname, timer = store.timerAtom,
}) })
server.removeSignalsFromClient(player, "surname") server.removeSignalsFromClient(player, "status")
``` ```
--- ---
@ -807,9 +841,11 @@ You can pass either writable signals, atoms, or [reactive objects](#reactiveinit
```luau ```luau
client.addSignals({ client.addSignals({
name = nameStore.setName, status = store.setStatus,
surname = nameStore.setSurname, timer = store.timerAtom,
age = nameStore.ageAtom, -- Signals unique to a player should also have a key unique to
-- that player, to avoid overwriting other synced signals
[`inventory-{Players.LocalPlayer.UserId}`] = clientStore.setInventory,
}) })
``` ```
@ -821,11 +857,11 @@ Unsubscribes from each signal with the corresponding keys. The signals will reta
```luau ```luau
client.addSignals({ client.addSignals({
name = nameStore.setName, status = store.setStatus,
surname = nameStore.setSurname, timer = store.timerAtom,
}) })
client.removeSignals("name") client.removeSignals("status")
``` ```
--- ---
@ -838,8 +874,9 @@ You should call `patch` when receiving updates from the server from a remote eve
```luau ```luau
client.addSignals({ client.addSignals({
name = nameStore.setName, status = store.setStatus,
surname = nameStore.setSurname, timer = store.timerAtom,
[`inventory-{Players.LocalPlayer.UserId}`] = clientStore.setInventory,
}) })
syncEvent.OnClientEvent:Connect(function(updates) syncEvent.OnClientEvent:Connect(function(updates)
@ -855,8 +892,8 @@ If you have a lot of signals to sync between the server and clients, it might be
```luau ```luau
local sharedState = { local sharedState = {
name = signalToAtom(nameStore.getName, nameStore.setName), status = signalToAtom(store.getStatus, store.setStatus),
surname = signalToAtom(nameStore.getSurname, nameStore.setSurname), inventories = signalToAtom(store.getInventories, store.setInventories),
} }
client.addSignals(sharedState) client.addSignals(sharedState)
@ -869,7 +906,7 @@ server.addSignalsToClient(client, sharedState)
Charm Sync will only send clients the differences between the current state and the last-synced state through a process called _delta compression_. In this case, tables are recursively scanned for changes, and unchanged properties are omitted by setting them to `nil`. Charm Sync will only send clients the differences between the current state and the last-synced state through a process called _delta compression_. In this case, tables are recursively scanned for changes, and unchanged properties are omitted by setting them to `nil`.
But it's hard to differentiate between an unchanged value and a removed value, as both cases are represented by `nil`. We chose to address this by representing deleted values with a special `None` symbol denoted by `{ __none = "__none" }`. But it's hard to differentiate between an unchanged value and a removed value, as both cases are represented by `nil`. Charm addresses this by representing deleted values with a special `None` value denoted by `{ __none = "__none" }`.
This means nilable values may be replaced with `None` in patches, and code working with update payloads (usually for remote argument serialization) should account for nilable values possibly being sent as `None` in the payload. This means nilable values may be replaced with `None` in patches, and code working with update payloads (usually for remote argument serialization) should account for nilable values possibly being sent as `None` in the payload.