feat(sync): add autoSerialize option (#42)

* feat(sync): add `autoSerialize` option

* docs: fix `autoSerialize` note

* docs: fix typo
This commit is contained in:
richard 2025-01-25 14:07:14 -08:00 • committed by GitHub
parent 8eddd182d3
commit b8bb123379
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 55 additions and 49 deletions

View file

@ -132,14 +132,13 @@ declare namespace CharmSync {
*/
preserveHistory?: boolean;
/**
* When `true`, convert problematic sparse arrays into dictionaries
* with string keys. Defaults to `true`.
* When `true`, Charm will apply validation and serialize unsafe arrays
* to address remote event argument limitations. Defaults to `true`.
*
* While this is mandatory for safely syncing arrays over vanilla Roblox
* remotes, it can be disabled if your network library uses a custom
* serialization method (i.e. Zap, ByteNet).
* This option should be disabled if your network library uses a custom
* serialization method (i.e. Zap, ByteNet) to prevent interference.
*/
serializeArrays?: boolean;
autoSerialize?: boolean;
}
interface ClientSyncer<Atoms extends AtomMap> {

View file

@ -45,10 +45,12 @@ end
@param prevState The previous state.
@param nextState The next state.
@param serializeArrays Whether to convert sparse arrays into dictionaries. Defaults to `true`.
@param serialize Whether to convert sparse arrays into dictionaries. Defaults to `true`.
@returns A patch that can be applied to the previous state.
]=]
local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serializeArrays: boolean?)
local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serialize: boolean?)
serialize = serialize ~= false
local patches = table.clone(nextState)
for key, previous in next, prevState do
@ -59,25 +61,19 @@ local function diff(prevState: { [any]: any }, nextState: { [any]: any }, serial
elseif current == nil then
patches[key] = None
elseif type(previous) == "table" and type(current) == "table" then
patches[key] = diff(previous, current, serializeArrays)
patches[key] = diff(previous, current, serialize)
end
end
-- Coerce sparse array patches into dictionaries
if serializeArrays ~= false and (prevState[1] ~= nil or nextState[1] ~= nil) then
if serialize and (prevState[1] ~= nil or nextState[1] ~= nil) then
patches = stringifySparseArray(patches)
end
if __DEV__ then
for key, value in next, prevState do
if serialize and __DEV__ then
for key, value in next, patches do
validate(value, key)
end
for key, value in nextState do
if prevState[key] ~= value then
validate(value, key)
end
end
end
return patches

View file

@ -24,14 +24,13 @@ type ServerOptions = {
]=]
preserveHistory: boolean?,
--[=[
Whether to convert problematic sparse arrays into dictionaries with
string keys. Defaults to `true`.
When `true`, Charm will apply validation and serialize unsafe arrays
to address remote event argument limitations. Defaults to `true`.
While this is mandatory for safely syncing arrays over vanilla Roblox
remotes, it can be disabled if your network library uses a custom
serialization method (i.e. Zap, ByteNet).
This option should be disabled if your network library uses a custom
serialization method (i.e. Zap, ByteNet) to prevent interference.
]=]
serializeArrays: boolean?,
autoSerialize: boolean?,
}
type ServerSyncer = {
@ -144,7 +143,7 @@ local function server(options: ServerOptions): ServerSyncer
table.insert(payloads, {
type = "patch",
data = patch.diff(snapshots[index - 1], snapshot, options.serializeArrays),
data = patch.diff(snapshots[index - 1], snapshot, options.autoSerialize),
})
end
@ -181,7 +180,7 @@ local function server(options: ServerOptions): ServerSyncer
table.insert(payloads, {
type = "patch",
data = patch.diff(snapshots[index - 1], snapshot, options.serializeArrays),
data = patch.diff(snapshots[index - 1], snapshot, options.autoSerialize),
})
end
@ -218,7 +217,7 @@ local function server(options: ServerOptions): ServerSyncer
local payload: SyncPayload = {
type = "patch",
data = patch.diff(previousSnapshot, currentSnapshot, options.serializeArrays),
data = patch.diff(previousSnapshot, currentSnapshot, options.autoSerialize),
}
previousSnapshot = table.clone(currentSnapshot)
@ -247,7 +246,7 @@ local function server(options: ServerOptions): ServerSyncer
local payload: SyncPayload = {
type = "patch",
data = patch.diff(previousSnapshot, currentSnapshot, options.serializeArrays),
data = patch.diff(previousSnapshot, currentSnapshot, options.autoSerialize),
}
previousSnapshot = table.clone(currentSnapshot)

View file

@ -1,5 +1,5 @@
local SAFE_KEYS = { string = true, number = true }
local SAFE_VALUES = { string = true, number = true, boolean = true, table = true, userdata = true, vector = true }
local UNSAFE_VALUES = { ["function"] = true, thread = true }
local function isTableUnsafe(object: { [any]: any })
local keyType = nil
@ -40,7 +40,7 @@ local function validate(value: any, key: any)
if not SAFE_KEYS[typeOfKey] then
error(`Invalid key type '{typeOfKey}' at key '{key}'`)
elseif not SAFE_VALUES[typeOfValue] then
elseif UNSAFE_VALUES[typeOfValue] then
error(`Invalid value type '{typeOfValue}' at key '{key}'`)
elseif typeOfValue == "table" then
if getmetatable(value) ~= nil then
@ -56,13 +56,7 @@ local function validate(value: any, key: any)
end
end
if typeOfValue == "number" then
if value ~= value then
error(`Cannot sync NaN at key '{key}'`)
elseif value == math.huge or value == -math.huge then
error(`Cannot sync infinity at key '{key}'`)
end
elseif typeOfKey == "number" then
if typeOfKey == "number" then
if key == math.huge or key == -math.huge then
error("Cannot sync infinity as key")
elseif key ~= math.floor(key) then