refactor: separate code for preserving history

This commit is contained in:
Richard 2024-09-25 22:10:19 -07:00
parent 8aef95e2ae
commit e5f655482f

View file

@ -62,69 +62,103 @@ type ServerSyncer = {
local function server(options: ServerOptions): ServerSyncer local function server(options: ServerOptions): ServerSyncer
local self = {} :: ServerSyncer local self = {} :: ServerSyncer
local sync: (player: Player, payload: SyncPayload) -> () local sync: (player: Player, payload: SyncPayload) -> ()
local snapshots: { { [string]: any } } = {}
local changed = false
-- Start the interval to send state patches over the network. local function createSnapshot()
function self:connect(callback) local snapshot: { [string]: any } = {}
local cleanups = {}
sync = callback
local function getSnapshot()
local snapshot: { [string]: any } = {}
for key, atom in next, options.atoms do
snapshot[key] = atom()
end
return snapshot
end
local function pushSnapshot(key: string, current: any, previous: any)
if not options.preserveHistory then
if snapshots[2] then
snapshots[2][key] = current
else
snapshots[2] = getSnapshot()
end
return
end
local lastSnapshot = snapshots[#snapshots]
local previousSnapshot = snapshots[#snapshots - 1]
-- Optimize snapshots by updating the most recent snapshot if the
-- previous and current values are the same, since this allows us
-- to group multiple changes into a single snapshot.
if previousSnapshot and previousSnapshot[key] == previous and lastSnapshot[key] == previous then
lastSnapshot[key] = current
else
local nextSnapshot = table.clone(lastSnapshot)
nextSnapshot[key] = current
table.insert(snapshots, nextSnapshot)
end
end
-- Populate the initial state and snapshot for each atom.
-- Subscribe to each atom and update the state when it changes.
for key, atom in next, options.atoms do for key, atom in next, options.atoms do
cleanups[key] = subscribe(atom, function(current, previous) snapshot[key] = atom()
pushSnapshot(key, current, previous)
changed = true
end)
end end
snapshots[1] = getSnapshot() return snapshot
end
if options.preserveHistory then
-- Preserve a history of every state change to allow for more granular
-- syncing with the client.
local snapshots: { { [string]: any } } = {}
local changed = false
function self:connect(callback)
local subscriptions = {}
sync = callback
local function pushSnapshot(key: string, current: any, previous: any)
local lastSnapshot = snapshots[#snapshots]
local previousSnapshot = snapshots[#snapshots - 1]
-- Optimize snapshots by updating the most recent snapshot if the
-- previous and current values are the same, since this allows us
-- to group multiple changes into a single snapshot.
if previousSnapshot and previousSnapshot[key] == previous and lastSnapshot[key] == previous then
lastSnapshot[key] = current
else
local nextSnapshot = table.clone(lastSnapshot)
nextSnapshot[key] = current
table.insert(snapshots, nextSnapshot)
end
end
-- Populate the snapshot with the initial state of each atom.
-- Subscribe to each atom and update the state when it changes.
for key, atom in next, options.atoms do
subscriptions[key] = subscribe(atom, function(current, previous)
pushSnapshot(key, current, previous)
changed = true
end)
end
snapshots[1] = createSnapshot()
local disconnect = setInterval(function()
if not changed then
return
end
local payloads: { SyncPayload } = {}
local lastSnapshot
for index, snapshot in next, snapshots do
lastSnapshot = snapshot
if index == 1 then
continue
end
table.insert(payloads, {
type = "patch",
data = patch.diff(snapshots[index - 1], snapshot),
})
end
snapshots = { lastSnapshot }
changed = false
for _, player in next, Players:GetPlayers() do
callback(player, unpack(payloads))
end
end, options.interval or 0)
return function()
disconnect()
for _, unsubscribe in next, subscriptions do
unsubscribe()
end
end
end
function self:_sendPatch(player: Player)
assert(sync, "connect() must be called before _sendPatch()")
local disconnect = setInterval(function()
if not changed then if not changed then
return return
end end
local payloads: { SyncPayload } = {} local payloads: { SyncPayload } = {}
local lastSnapshot
for index, snapshot in next, snapshots do for index, snapshot in next, snapshots do
lastSnapshot = snapshot
if index == 1 then if index == 1 then
continue continue
end end
@ -135,19 +169,73 @@ local function server(options: ServerOptions): ServerSyncer
}) })
end end
snapshots = { lastSnapshot } snapshots = { snapshots[#snapshots] }
changed = false changed = false
for _, player in next, Players:GetPlayers() do sync(player, unpack(payloads))
callback(player, unpack(payloads)) end
end else
end, options.interval or 0) -- Only send the most recent state to the client.
local previousSnapshot = createSnapshot()
local currentSnapshot = table.clone(previousSnapshot)
local changed = false
return function() function self:connect(callback)
disconnect() local subscriptions = {}
for _, cleanup in next, cleanups do
cleanup() sync = callback
-- Subscribe to each atom and update the state when one changes.
for key, atom in next, options.atoms do
subscriptions[key] = subscribe(atom, function(state)
currentSnapshot[key] = state
changed = true
end)
end end
local disconnect = setInterval(function()
if not changed then
return
end
local payload: SyncPayload = {
type = "patch",
data = patch.diff(previousSnapshot, currentSnapshot),
}
previousSnapshot = table.clone(currentSnapshot)
changed = false
for _, player in next, Players:GetPlayers() do
callback(player, payload)
end
end, options.interval or 0)
return function()
disconnect()
for _, unsubscribe in next, subscriptions do
unsubscribe()
end
end
end
function self:_sendPatch(player: Player)
assert(sync, "connect() must be called before _sendPatch()")
if not changed then
return
end
local payload: SyncPayload = {
type = "patch",
data = patch.diff(previousSnapshot, currentSnapshot),
}
previousSnapshot = table.clone(currentSnapshot)
changed = false
sync(player, payload)
end end
end end
@ -157,36 +245,10 @@ local function server(options: ServerOptions): ServerSyncer
sync(player, { sync(player, {
type = "init", type = "init",
data = snapshots[#snapshots], data = createSnapshot(),
}) })
end end
function self:_sendPatch(player: Player)
assert(sync, "connect() must be called before _sendPatch()")
if not changed then
return
end
local payloads: { SyncPayload } = {}
for index, snapshot in next, snapshots do
if index == 1 then
continue
end
table.insert(payloads, {
type = "patch",
data = patch.diff(snapshots[index - 1], snapshot),
})
end
snapshots = { snapshots[#snapshots] }
changed = false
sync(player, unpack(payloads))
end
return self return self
end end