mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
feat: add signal api
This commit is contained in:
parent
bf92413a97
commit
1e0b10aef6
21 changed files with 300 additions and 293 deletions
|
|
@ -24,7 +24,7 @@ local function mockArraySerdes(object: { [any]: any }): { [any]: any }
|
|||
end
|
||||
|
||||
afterEach(function()
|
||||
config.autoSerialize = true
|
||||
config.fixArrays = true
|
||||
config.validatePatches = true
|
||||
end)
|
||||
|
||||
|
|
@ -104,13 +104,13 @@ test("cannot preserve sparse arrays", function()
|
|||
end)
|
||||
|
||||
test("optionally fixes sparse arrays", function()
|
||||
config.autoSerialize = true
|
||||
config.fixArrays = true
|
||||
|
||||
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }) do
|
||||
assert(type(index) == "string", `expected {type(index)} {index} to be string`)
|
||||
end
|
||||
|
||||
config.autoSerialize = false
|
||||
config.fixArrays = false
|
||||
|
||||
for index in next, patch.diff({ 1, 1, 1, 1, 1 }, { 2, 1, 1, 2 }) do
|
||||
assert(type(index) == "number", `expected {type(index)} {index} to be number`)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ local function syncAtoms(atoms: { [string]: Charm.Atom })
|
|||
warn("[Charm] Attempted to sync different atoms with the same key:", key)
|
||||
continue
|
||||
end
|
||||
|
||||
atomRegister[key] = atom
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ return {
|
|||
--- to address remote event argument limitations. Defaults to `true`.
|
||||
--- This option should be disabled if your network library uses a custom
|
||||
--- serialization method (i.e. Zap, ByteNet) to prevent interference.
|
||||
autoSerialize = true,
|
||||
fixArrays = true,
|
||||
--- When `true`, patches will be validated before being sent to clients to
|
||||
--- enforce remote event limitations. Only checked in strict mode and if
|
||||
--- `autoSerialize` is enabled. Defaults to `true`.
|
||||
--- `fixArrays` is enabled. Defaults to `true`.
|
||||
validatePatches = true,
|
||||
}
|
||||
|
|
|
|||
11
packages/charm-sync/src/index.d.ts
vendored
11
packages/charm-sync/src/index.d.ts
vendored
|
|
@ -1,7 +1,5 @@
|
|||
import { Atom } from "@rbxts/charm";
|
||||
|
||||
type Cleanup = () => void;
|
||||
|
||||
type Key = string | number | symbol;
|
||||
|
||||
/**
|
||||
|
|
@ -18,7 +16,7 @@ type SelectorMap = Record<string, () => any>;
|
|||
/**
|
||||
* Infers the type of the return values produced by a map of functions.
|
||||
*/
|
||||
type StateOfMap<T> = {
|
||||
export type StateOfMap<T> = {
|
||||
readonly [P in keyof T]: T[P] extends () => infer State ? State : never;
|
||||
};
|
||||
|
||||
|
|
@ -88,11 +86,11 @@ export const config: {
|
|||
*
|
||||
* @server
|
||||
*/
|
||||
autoSerialize: boolean;
|
||||
fixArrays: boolean;
|
||||
/**
|
||||
* When `true`, patches will be validated before being sent to clients to
|
||||
* enforce remote event limitations. Only checked in strict mode and if
|
||||
* `autoSerialize` is enabled. Defaults to `true`.
|
||||
* `fixArrays` is enabled. Defaults to `true`.
|
||||
*
|
||||
* @server
|
||||
*/
|
||||
|
|
@ -178,11 +176,10 @@ export namespace server {
|
|||
* be sent to clients whenever the atoms are updated.
|
||||
*
|
||||
* @param onSync The function to call when patches can be sent to a client.
|
||||
* @returns `stopSync()`
|
||||
*/
|
||||
export function startSync<Atoms extends { [key: string]: Atom<any> }, Serialize extends boolean = true>(
|
||||
onSync: (client: Player, payloads: SyncPayload<Atoms, Serialize>[]) => void,
|
||||
): Cleanup;
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Stops automatically syncing registered atoms to clients. Patches will
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ local config = require("./config")
|
|||
local types = require("./types")
|
||||
local validate = require("./validate")
|
||||
|
||||
local flags = Charm.flags
|
||||
local globals = Charm.globals
|
||||
|
||||
export type None = types.None
|
||||
|
||||
|
|
@ -73,13 +73,13 @@ local function diff(currentState: any, nextState: any): any
|
|||
patches[key] = diff(previous, nextState[key])
|
||||
end
|
||||
|
||||
if config.autoSerialize then
|
||||
if config.fixArrays then
|
||||
-- Coerce sparse array patches into dictionaries
|
||||
if currentState[1] ~= nil or nextState[1] ~= nil then
|
||||
patches = stringifySparseArray(patches)
|
||||
end
|
||||
|
||||
if flags.strict and config.validatePatches then
|
||||
if globals.strict and config.validatePatches then
|
||||
for key, value in patches do
|
||||
validate(value, key)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ local types = require("./types")
|
|||
|
||||
type SyncPayload = types.SyncPayload
|
||||
|
||||
local globals = Charm.globals
|
||||
|
||||
type RegisteredAtom<T = any> = {
|
||||
atom: () -> T,
|
||||
value: T,
|
||||
valueLastSynced: T,
|
||||
stop: () -> (),
|
||||
callback: () -> T,
|
||||
currentValue: T,
|
||||
previousValue: T,
|
||||
dispose: () -> (),
|
||||
}
|
||||
|
||||
type SyncStatus = number
|
||||
|
|
@ -19,82 +21,82 @@ local LISTENING_FOR_CHANGES: SyncStatus = 1
|
|||
|
||||
local atomRegister: { [string]: RegisteredAtom } = {}
|
||||
local clientRegister: { [any]: { [string]: SyncStatus } } = {}
|
||||
local globalUpdates: { { key: string, value: any, previousValue: any } } = {}
|
||||
local globalUpdates: { { key: string, currentValue: any, previousValue: any } } = {}
|
||||
|
||||
local connection: RBXScriptConnection?
|
||||
local onSyncCallback: (client: any, payloads: { SyncPayload }) -> ()?
|
||||
|
||||
local function registerAtom<T>(key: string, atom: () -> T)
|
||||
local function syncGlobalAtom<T>(key: string, callback: () -> T)
|
||||
if atomRegister[key] then
|
||||
if atomRegister[key].atom ~= atom then
|
||||
if atomRegister[key].callback ~= callback then
|
||||
warn("[Charm] Attempted to sync different atoms with the same key:", key)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local registeredAtom: RegisteredAtom<T> = {
|
||||
atom = atom,
|
||||
value = atom(),
|
||||
valueLastSynced = atom(),
|
||||
stop = function() end,
|
||||
callback = callback,
|
||||
currentValue = callback(),
|
||||
previousValue = callback(),
|
||||
dispose = function() end,
|
||||
}
|
||||
|
||||
registeredAtom.stop = Charm.effect(function()
|
||||
local value = atom()
|
||||
registeredAtom.dispose = Charm.effect(function()
|
||||
local newValue = callback()
|
||||
|
||||
-- When preserving history, we need to keep track of previous values
|
||||
-- so we can create granular patches for each change.
|
||||
if config.preserveHistory and registeredAtom.value ~= value then
|
||||
if config.preserveHistory and registeredAtom.currentValue ~= newValue then
|
||||
table.insert(globalUpdates, {
|
||||
key = key,
|
||||
value = value,
|
||||
previousValue = registeredAtom.value,
|
||||
currentValue = newValue,
|
||||
previousValue = registeredAtom.currentValue,
|
||||
})
|
||||
end
|
||||
|
||||
registeredAtom.value = value
|
||||
registeredAtom.currentValue = newValue
|
||||
end)
|
||||
|
||||
atomRegister[key] = registeredAtom
|
||||
end
|
||||
|
||||
local function unregisterAtom(key: string)
|
||||
local function unsyncGlobalAtom(key: string)
|
||||
local registeredAtom = atomRegister[key]
|
||||
|
||||
if registeredAtom then
|
||||
registeredAtom.stop()
|
||||
registeredAtom.dispose()
|
||||
atomRegister[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function unregisterUnusedAtoms()
|
||||
local used: { [string]: boolean } = {}
|
||||
local function unsyncStaleGlobalAtoms()
|
||||
local active: { [string]: boolean } = {}
|
||||
|
||||
for _, clientAtoms in clientRegister do
|
||||
for key in clientAtoms do
|
||||
used[key] = true
|
||||
active[key] = true
|
||||
end
|
||||
end
|
||||
|
||||
for key in atomRegister do
|
||||
if not used[key] then
|
||||
unregisterAtom(key)
|
||||
if not active[key] then
|
||||
unsyncGlobalAtom(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function createHistoricalPatches(): { any }
|
||||
local function createGlobalPatchHistory(): { any }
|
||||
local patchCount = #globalUpdates
|
||||
local patches = table.create(patchCount)
|
||||
|
||||
for index, update in globalUpdates do
|
||||
patches[index] = patch.diff(update.previousValue, update.value)
|
||||
patches[index] = patch.diff(update.previousValue, update.currentValue)
|
||||
end
|
||||
|
||||
return patches
|
||||
end
|
||||
|
||||
local function createHistoricalPayloadsForClient(client: any, patches: { any }): { SyncPayload }
|
||||
local function createPayloadHistoryForClient(client: any, patches: { any }): { SyncPayload }
|
||||
local syncedAtoms = clientRegister[client]
|
||||
|
||||
if not syncedAtoms then
|
||||
|
|
@ -123,9 +125,8 @@ local function createHistoricalPayloadsForClient(client: any, patches: { any }):
|
|||
elseif status == PENDING_INITIAL_STATE then
|
||||
table.insert(payloads, {
|
||||
type = "init",
|
||||
data = { [key] = update.value },
|
||||
data = { [key] = update.currentValue },
|
||||
})
|
||||
|
||||
syncedAtoms[key] = LISTENING_FOR_CHANGES
|
||||
end
|
||||
end
|
||||
|
|
@ -138,7 +139,7 @@ local function createHistoricalPayloadsForClient(client: any, patches: { any }):
|
|||
local registeredAtom = atomRegister[key]
|
||||
|
||||
if status == PENDING_INITIAL_STATE and registeredAtom then
|
||||
initPayload[key] = registeredAtom.value
|
||||
initPayload[key] = registeredAtom.currentValue
|
||||
syncedAtoms[key] = LISTENING_FOR_CHANGES
|
||||
end
|
||||
end
|
||||
|
|
@ -150,20 +151,20 @@ local function createHistoricalPayloadsForClient(client: any, patches: { any }):
|
|||
return payloads
|
||||
end
|
||||
|
||||
local function createPatch(): any
|
||||
local function createGlobalPatch(): any
|
||||
local current: { [string]: any } = {}
|
||||
local previous: { [string]: any } = {}
|
||||
|
||||
for key, registeredAtom in atomRegister do
|
||||
current[key] = registeredAtom.value
|
||||
previous[key] = registeredAtom.valueLastSynced
|
||||
registeredAtom.valueLastSynced = registeredAtom.value
|
||||
current[key] = registeredAtom.currentValue
|
||||
previous[key] = registeredAtom.previousValue
|
||||
registeredAtom.previousValue = registeredAtom.currentValue
|
||||
end
|
||||
|
||||
return patch.diff(previous, current)
|
||||
end
|
||||
|
||||
local function createPayloadsForClient(client: any, patchData: any): { SyncPayload }
|
||||
local function createPayloadForClient(client: any, patchData: any): { SyncPayload }
|
||||
local syncedAtoms = clientRegister[client]
|
||||
local payloads: { SyncPayload } = {}
|
||||
|
||||
|
|
@ -182,7 +183,7 @@ local function createPayloadsForClient(client: any, patchData: any): { SyncPaylo
|
|||
if status == LISTENING_FOR_CHANGES then
|
||||
patchPayload[key] = patchData[key]
|
||||
elseif status == PENDING_INITIAL_STATE then
|
||||
initPayload[key] = registeredAtom.value
|
||||
initPayload[key] = registeredAtom.currentValue
|
||||
syncedAtoms[key] = LISTENING_FOR_CHANGES
|
||||
end
|
||||
end
|
||||
|
|
@ -215,7 +216,7 @@ local function syncAtomsToClient(client: any, atoms: { [string]: () -> any })
|
|||
local syncedAtoms = clientRegister[client]
|
||||
|
||||
for key, atom in atoms do
|
||||
registerAtom(key, atom)
|
||||
syncGlobalAtom(key, atom)
|
||||
|
||||
if not syncedAtoms[key] then
|
||||
-- Mark the atom as pending initial state so we send its full value
|
||||
|
|
@ -263,18 +264,18 @@ local function flush()
|
|||
return
|
||||
end
|
||||
|
||||
local patches = if config.preserveHistory then createHistoricalPatches() else createPatch()
|
||||
local patches = if config.preserveHistory then createGlobalPatchHistory() else createGlobalPatch()
|
||||
|
||||
-- While payload objects are technically mutable as they are unique to
|
||||
-- each client, the data inside them is not safe to mutate.
|
||||
if Charm.flags.frozen then
|
||||
if globals.frozen then
|
||||
Charm.deepFreeze(patches)
|
||||
end
|
||||
|
||||
for client in clientRegister do
|
||||
local payloads: { SyncPayload } = if config.preserveHistory
|
||||
then createHistoricalPayloadsForClient(client, patches)
|
||||
else createPayloadsForClient(client, patches)
|
||||
then createPayloadHistoryForClient(client, patches)
|
||||
else createPayloadForClient(client, patches)
|
||||
|
||||
if payloads[1] then
|
||||
if task then
|
||||
|
|
@ -285,10 +286,43 @@ local function flush()
|
|||
end
|
||||
end
|
||||
|
||||
unregisterUnusedAtoms()
|
||||
unsyncStaleGlobalAtoms()
|
||||
table.clear(globalUpdates)
|
||||
end
|
||||
|
||||
--- Starts automatically syncing registered atoms to clients. Patches will
|
||||
--- be sent to clients whenever the atoms are updated.
|
||||
---
|
||||
--- @param onSync The function to call when patches can be sent to a client.
|
||||
local function startSync(onSync: (client: any, payloads: { SyncPayload }) -> ())
|
||||
if connection then
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
onSyncCallback = onSync
|
||||
|
||||
if not game then
|
||||
return
|
||||
end
|
||||
|
||||
local timer = 0
|
||||
|
||||
connection = game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
|
||||
if config.interval < 0 then
|
||||
return
|
||||
end
|
||||
|
||||
timer += deltaTime
|
||||
|
||||
if timer >= config.interval then
|
||||
timer = 0
|
||||
debug.profilebegin("CharmSync")
|
||||
flush()
|
||||
debug.profileend()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Stops automatically syncing registered atoms to clients. Patches will
|
||||
--- no longer be sent to clients until `startSync` is called again.
|
||||
local function stopSync()
|
||||
|
|
@ -307,42 +341,6 @@ local function reset()
|
|||
stopSync()
|
||||
end
|
||||
|
||||
--- Starts automatically syncing registered atoms to clients. Patches will
|
||||
--- be sent to clients whenever the atoms are updated.
|
||||
---
|
||||
--- @param onSync The function to call when patches can be sent to a client.
|
||||
--- @return `stopSync()`
|
||||
local function startSync(onSync: (client: any, payloads: { SyncPayload }) -> ()): () -> ()
|
||||
if connection then
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
onSyncCallback = onSync
|
||||
|
||||
if not game then
|
||||
return stopSync
|
||||
end
|
||||
|
||||
local timer = 0
|
||||
|
||||
connection = game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
|
||||
if config.interval < 0 then
|
||||
return
|
||||
end
|
||||
|
||||
timer += deltaTime
|
||||
|
||||
if timer >= config.interval then
|
||||
timer = 0
|
||||
debug.profilebegin("CharmSync")
|
||||
flush()
|
||||
debug.profileend()
|
||||
end
|
||||
end)
|
||||
|
||||
return stopSync
|
||||
end
|
||||
|
||||
return {
|
||||
syncAtomsToClient = syncAtomsToClient,
|
||||
unsyncClient = unsyncClient,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue