feat: add state sync

This commit is contained in:
Richard 2024-02-27 22:20:18 -08:00
parent 5fe86ef030
commit 17a8c48e54
9 changed files with 262 additions and 0 deletions

31
src/sync/client.luau Normal file
View file

@ -0,0 +1,31 @@
local atom = require(script.Parent.Parent.atom)
type Atom<T> = atom.Atom<T>
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type ClientSyncOptions = {
atoms: { [string]: Atom<any> },
}
type ClientSync = {
sync: (self: ClientSync, payload: types.SyncPayload) -> (),
}
local function client(options: ClientSyncOptions): ClientSync
local atoms = options.atoms
local self = {} :: ClientSync
function self:sync(payload)
for key, state in payload.data do
if payload.type == "patch" then
atoms[key]:set(patch.apply(atoms[key]:get(), state))
else
atoms[key]:set(state)
end
end
end
return self
end
return client

10
src/sync/init.luau Normal file
View file

@ -0,0 +1,10 @@
local client = require(script.client)
local server = require(script.server)
local types = require(script.types)
export type SyncPayload = types.SyncPayload
return {
client = client,
server = server,
}

42
src/sync/patch.luau Normal file
View file

@ -0,0 +1,42 @@
local NONE = { __none = "__none" }
local function diff(previousState: { [any]: any }, nextState: { [any]: any })
local patch = table.clone(nextState)
for key, previous in previousState do
local next = nextState[key]
if previous == next then
patch[key] = nil
elseif next == nil then
patch[key] = NONE
elseif type(previous) == "table" and type(next) == "table" then
patch[key] = diff(previous, next)
end
end
return patch
end
local function apply(state: { [any]: any }, patchState: { [any]: any })
local nextState = table.clone(state)
for key, patch in patchState do
local current = nextState[key]
if type(patch) == "table" and patch.__none == "__none" then
nextState[key] = nil
elseif type(patch) == "table" and type(current) == "table" then
nextState[key] = apply(current, patch)
else
nextState[key] = patch
end
end
return nextState
end
return {
diff = diff,
apply = apply,
}

77
src/sync/server.luau Normal file
View file

@ -0,0 +1,77 @@
local Players = game:GetService("Players")
local atom = require(script.Parent.Parent.atom)
type Atom<T> = atom.Atom<T>
local setInterval = require(script.Parent.Parent.utils.setInterval)
local patch = require(script.Parent.patch)
local types = require(script.Parent.types)
type ServerSyncOptions = {
atoms: { [string]: Atom<any> },
interval: number?,
}
type ServerSync = {
onSync: (self: ServerSync, callback: (player: Player, payload: types.SyncPayload) -> ()) -> () -> (),
flush: (self: ServerSync, player: Player?) -> (),
hydrate: (self: ServerSync, player: Player) -> (),
}
local function server(options: ServerSyncOptions): ServerSync
local atoms = options.atoms
local interval = options.interval or 0
local self = {} :: ServerSync
local sync: (player: Player, payload: types.SyncPayload) -> ()
local state: {}
local function getState()
local state = {}
for key, atom in atoms do
state[key] = atom:get()
end
return state
end
function self:onSync(callback)
sync = callback
state = getState()
return setInterval(function()
self:flush()
end, interval)
end
function self:flush(player: Player?)
local current = getState()
local previous = state or current
state = current
if current ~= previous then
local payload: types.SyncPayload = {
type = "patch",
data = patch.diff(previous, current),
}
if player then
return sync(player, payload)
end
for _, player in Players:GetPlayers() do
sync(player, payload)
end
end
end
function self:hydrate(player)
assert(sync, "onSync must be called before onJoin")
sync(player, {
type = "set",
data = getState(),
})
end
return self
end
return server

6
src/sync/types.luau Normal file
View file

@ -0,0 +1,6 @@
export type SyncPayload = {
type: "patch" | "set",
data: { [any]: any },
}
return nil