diff --git a/README.md b/README.md index c337ff1..da3fd8f 100644 --- a/README.md +++ b/README.md @@ -640,25 +640,19 @@ return { } ``` -When a player joins on the server, call `server.addSignalsToClient` with the keyed 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 -local function onPlayerAdded(player: Player) - -- Add signal getters, computed signals, atoms, or reactive objects +playerReadyEvent.OnServerEvent:Connect(function(player) + -- Sync signal getters, computed signals, atoms, or reactive proxies server.addSignalsToClient(player, { name = nameStore.getName, surname = nameStore.getSurname, age = nameStore.ageAtom, }) -end - -for _, player in Players:GetPlayers() do - onPlayerAdded(player) -end - -Players.PlayerAdded:Connect(onPlayerAdded) +end) Players.PlayerRemoving:Connect(function(player) server.removeClient(player) @@ -671,7 +665,7 @@ end) ``` > [!NOTE] -> On the server, make sure each key corresponds to the same signal 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 data across all players. If two players subscribe to the same key, but were given different signals, Charm will output a warning. 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. @@ -701,8 +695,8 @@ A configuration table that customizes the behavior of Charm Sync on the server. | --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | interval | `0` | The frequency at which the server will send patches to the client, in seconds. A value of `0` sends updates on the next frame. Set to a negative value to disable the interval. | | preserveHistory | `false` | Whether to preserve a full history of state changes since the last sync, at the cost of performance. This is useful if you need to replicate each individual change that occurs between sync events. | -| fixArrays | `true` | When `true`, Charm will attempt to work around Roblox remote event limitations regarding array patches. Disable this if your networking library serializes remote arguments (Zap, ByteNet, etc.). | -| validatePatches | `true` | When `true`, and both `fixArrays` and strict mode are enabled, synced values containing unsafe sparse arrays or mixed tables will throw an error. See the [remote argument limitations](https://create.roblox.com/docs/scripting/events/remote#argument-limitations). | +| fixArrays | `true` | When `true`, Charm will attempt to work around remote event limitations that cause data loss in array patches. Disable this if your networking library serializes remote arguments (Zap, ByteNet, etc.). | +| validatePatches | `true` | When `true`, and both `fixArrays` and strict mode are enabled, synced values containing unsafe sparse arrays or mixed tables will emit a warning. See the [remote argument limitations](https://create.roblox.com/docs/scripting/events/remote#argument-limitations). | --- @@ -713,7 +707,7 @@ The `addSignalsToClient` function subscribes a client to updates in the given si 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. ```luau -Players.PlayerAdded:Connect(function(player) +playerReadyEvent.OnServerEvent:Connect(function(player) server.addSignalsToClient(player, { name = nameStore.getName, surname = nameStore.getSurname, @@ -722,14 +716,22 @@ Players.PlayerAdded:Connect(function(player) end) ``` -You're also allowed to create new signals to sync to specific players, as long as the key is unique to that player: +You're also allowed to create new signals to sync to specific players, as long as the key is unique to that data: ```luau -server.addSignalsToClient(player, { - [`data-{player.UserId}`] = computed(function() - return getPlayerData(player.UserId) - end), -}) +playerReadyEvent.OnServerEvent:Connect(function(player) + server.addSignalsToClient(player, { + -- 🟢 Good: Player-specific data is synced with a unique key + [`data-{player.UserId}`] = computed(function() + return getPlayerData(player.UserId) + end), + + -- 🔴 Bad: Syncing different data with the same keys does not work + playerData = computed(function() + return getPlayerData(player.UserId) + end), + }) +end) ``` --- @@ -754,7 +756,7 @@ server.removeSignalsFromClient(player, "surname") The `removeClient` function unsubscribes a client from receiving all state updates from the server. You should call this function when a player leaves the game. ```luau -Players.PlayerAdded:Connect(function(player) +playerReadyEvent.OnServerEvent:Connect(function(player) server.addSignalsToClient(player, signals) end) diff --git a/packages/charm-sync/src/config.luau b/packages/charm-sync/src/config.luau index a101f05..c3ce27c 100644 --- a/packages/charm-sync/src/config.luau +++ b/packages/charm-sync/src/config.luau @@ -9,19 +9,22 @@ return { Whether the history of state changes since the client's last update should be preserved. This is useful for values that change multiple times per frame, where each individual change is important. Defaults - to `false`. + to `false` for performance reasons. ]] preserveHistory = false, --[[ - When `true`, Charm will apply validation and serialize unsafe arrays - to address remote event argument limitations. Defaults to `true`. + When `true`, Charm will modify sparse arrays to prevent data loss from + remote event limitations. When an unsafe array is detected, numeric + indices will be converted to string keys so that the array can be sent + safely and then converted back to an array on the receiving end. + This option should be disabled if your network library uses a custom serialization method (i.e. Zap, ByteNet) to prevent interference. ]] fixArrays = true, --[[ When `true`, synced state containing unsafe sparse arrays or mixed - tables will throw an error. Only checked in strict mode and if + tables will emit a warning. Only checked in strict mode and if `fixArrays` is enabled. Defaults to `true`. ]] validatePatches = true, diff --git a/packages/charm-sync/src/index.d.ts b/packages/charm-sync/src/index.d.ts index 6519951..bb40eba 100644 --- a/packages/charm-sync/src/index.d.ts +++ b/packages/charm-sync/src/index.d.ts @@ -78,14 +78,17 @@ export const config: { * Whether the history of state changes since the client's last update * should be preserved. This is useful for values that change multiple * times per frame, where each individual change is important. Defaults - * to `false`. + * to `false` for performance reasons. * * @server */ preserveHistory: boolean; /** - * When `true`, Charm will apply validation and serialize unsafe arrays - * to address remote event argument limitations. Defaults to `true`. + * When `true`, Charm will modify sparse arrays to prevent data loss from + * remote event limitations. When an unsafe array is detected, numeric + * indices will be converted to string keys so that the array can be sent + * safely and then converted back to an array on the receiving end. + * * This option should be disabled if your network library uses a custom * serialization method (i.e. Zap, ByteNet) to prevent interference. * @@ -94,7 +97,7 @@ export const config: { fixArrays: boolean; /** * When `true`, synced state containing unsafe sparse arrays or mixed - * tables will throw an error. Only checked in strict mode and if + * tables will emit a warning. Only checked in strict mode and if * `fixArrays` is enabled. Defaults to `true`. * * @server @@ -242,7 +245,7 @@ export namespace patch { * @param statePatch The patches to apply. * @returns The new state with the patch applied. */ - export function apply( + export function applyImmutable( currentState: State, statePatch: SyncPatch, ): State; diff --git a/packages/charm/test/effect.test.luau b/packages/charm/test/effect.test.luau index 5eb4c94..def4c68 100644 --- a/packages/charm/test/effect.test.luau +++ b/packages/charm/test/effect.test.luau @@ -497,7 +497,7 @@ end) test("recursive effects should run and cleanup in order", function() local a, setA = signal(0) - local order = {} + local order: { string } = {} effect(function() local value = a() diff --git a/packages/charm/test/observe.test.luau b/packages/charm/test/observe.test.luau index c328f16..223b1b0 100644 --- a/packages/charm/test/observe.test.luau +++ b/packages/charm/test/observe.test.luau @@ -91,7 +91,7 @@ end) test("should handle recursive additions and removals", function() local items, setItems = signal({ 1 }) - local edits = {} + local edits: { string } = {} observe(items, function(item) if item < 5 then @@ -114,7 +114,7 @@ end) test("should handle self removal", function() local items, setItems = signal({ 1, 2, 3, 4 }) - local edits = {} + local edits: { string } = {} observe(items, function(item, index) setItems(function(state: { number }) diff --git a/packages/charm/test/subscribe.test.luau b/packages/charm/test/subscribe.test.luau index 6936c7a..10ab932 100644 --- a/packages/charm/test/subscribe.test.luau +++ b/packages/charm/test/subscribe.test.luau @@ -46,7 +46,7 @@ test("should run in untracked", function() end) test("should ensure correct execution order", function() - local order = {} + local order: { number } = {} local n1, setN1 = signal(0) local n2, setN2 = signal(0) local sum = computed(function() @@ -72,7 +72,7 @@ test("should ensure correct execution order", function() end) test("should skip redundant updates", function() - local order = {} + local order: { "even" | "odd" } = {} local a, setA = signal(0) subscribe(function(): "even" | "odd"