mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 19:54:09 +00:00
feat: sync signals that return reactive proxy
This commit is contained in:
parent
aebe3d27ee
commit
b3a76face9
5 changed files with 69 additions and 71 deletions
|
|
@ -5,11 +5,10 @@ local types = require("./types")
|
||||||
type ReactiveProxy<T = {}> = Charm.ReactiveProxy<T>
|
type ReactiveProxy<T = {}> = Charm.ReactiveProxy<T>
|
||||||
type SyncPayload = types.SyncPayload
|
type SyncPayload = types.SyncPayload
|
||||||
|
|
||||||
type SignalSetter<T = any> = (T | (T) -> T) -> ()
|
type Setter<T = any> = (T | (T) -> any) -> ()
|
||||||
type ReactiveSetter<T = any> = (T | (T) -> ()) -> ()
|
|
||||||
|
|
||||||
local trackedSignals: { [string]: SignalSetter } = {}
|
local trackedSignals: { [string]: Setter } = {}
|
||||||
local trackedReactives: { [string]: ReactiveSetter } = {}
|
local trackedReactives: { [string]: Setter } = {}
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Registers a map of setters to the state with the corresponding keys on the
|
Registers a map of setters to the state with the corresponding keys on the
|
||||||
|
|
@ -22,7 +21,7 @@ local trackedReactives: { [string]: ReactiveSetter } = {}
|
||||||
|
|
||||||
@param signals A map of setter functions to sync with the server.
|
@param signals A map of setter functions to sync with the server.
|
||||||
]]
|
]]
|
||||||
local function addSignals(signals: { [string]: SignalSetter | {} })
|
local function addSignals(signals: { [string]: Setter | {} })
|
||||||
for key, signal in signals do
|
for key, signal in signals do
|
||||||
if type(signal) == "function" then
|
if type(signal) == "function" then
|
||||||
trackedSignals[key] = signal
|
trackedSignals[key] = signal
|
||||||
|
|
@ -55,7 +54,7 @@ end
|
||||||
|
|
||||||
local function applyInitPayload(data: { [string]: any })
|
local function applyInitPayload(data: { [string]: any })
|
||||||
for key, value in data do
|
for key, value in data do
|
||||||
local trackedSignal: SignalSetter? = trackedSignals[key] or trackedReactives[key]
|
local trackedSignal: Setter? = trackedSignals[key] or trackedReactives[key]
|
||||||
if trackedSignal then
|
if trackedSignal then
|
||||||
trackedSignal(value)
|
trackedSignal(value)
|
||||||
end
|
end
|
||||||
|
|
@ -72,7 +71,7 @@ local function applyPatchPayload(data: { [string]: any })
|
||||||
return patch.apply(value, diffs)
|
return patch.apply(value, diffs)
|
||||||
end)
|
end)
|
||||||
elseif trackedReactive then
|
elseif trackedReactive then
|
||||||
trackedReactive(function(value)
|
trackedReactive(function(value): ()
|
||||||
patch.applyMutable(value, diffs)
|
patch.applyMutable(value, diffs)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,16 @@ local function apply(oldState: any, diffs: any): any
|
||||||
return newState
|
return newState
|
||||||
end
|
end
|
||||||
|
|
||||||
local function applyMutable(targetState: any, sourceState: any): any
|
local function applyMutable(targetState: any, diffs: any): any
|
||||||
if isNone(sourceState) then
|
if isNone(diffs) then
|
||||||
return nil
|
return nil
|
||||||
elseif type(targetState) ~= "table" or type(sourceState) ~= "table" then
|
elseif type(targetState) ~= "table" or type(diffs) ~= "table" then
|
||||||
return sourceState
|
return diffs
|
||||||
end
|
end
|
||||||
|
|
||||||
local targetIsArray = targetState[1] ~= nil
|
local targetIsArray = targetState[1] ~= nil
|
||||||
|
|
||||||
for key, value in sourceState do
|
for key, value in diffs do
|
||||||
if targetIsArray and type(key) == "string" then
|
if targetIsArray and type(key) == "string" then
|
||||||
key = tonumber(key) or key
|
key = tonumber(key) or key
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ type SyncStatus = number
|
||||||
type ClientKeys = { [string]: SyncStatus }
|
type ClientKeys = { [string]: SyncStatus }
|
||||||
|
|
||||||
type TrackedState<T = any> = {
|
type TrackedState<T = any> = {
|
||||||
target: any,
|
identifier: unknown,
|
||||||
currentValue: T,
|
currentValue: T,
|
||||||
lastSyncedValue: T,
|
lastSyncedValue: T,
|
||||||
isReactive: boolean?,
|
isReactive: boolean,
|
||||||
dispose: () -> (),
|
dispose: () -> (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,75 +35,54 @@ local clients: { [any]: ClientKeys } = {}
|
||||||
local connection: RBXScriptConnection?
|
local connection: RBXScriptConnection?
|
||||||
local syncCallback: (client: any, payloads: { SyncPayload }) -> ()?
|
local syncCallback: (client: any, payloads: { SyncPayload }) -> ()?
|
||||||
|
|
||||||
local function trackSignal<T>(key: string, getter: () -> T)
|
local function trackSignal<T>(key: string, getter: () -> T, identifier: unknown)
|
||||||
if trackedStates[key] then
|
if trackedStates[key] then
|
||||||
-- Ignore duplicates, but warn against name collisions
|
-- Ignore duplicates, but warn against name collisions
|
||||||
if trackedStates[key].target ~= getter then
|
if trackedStates[key].identifier ~= identifier then
|
||||||
warn("[Charm] Attempted to sync different states with the same key:", key)
|
warn("[Charm] Attempted to sync different states with the same key:", key)
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local value = getter()
|
local trackedState: TrackedState<T>
|
||||||
local trackedState = {
|
local initialValue = getter()
|
||||||
target = getter,
|
local rawInitialValue = Charm.toRaw(initialValue)
|
||||||
currentValue = value,
|
local initialIsReactive = initialValue ~= rawInitialValue
|
||||||
lastSyncedValue = value,
|
local snapshot = if initialIsReactive then utils.deepCopy(rawInitialValue) else initialValue
|
||||||
}
|
|
||||||
|
local dispose = Charm.effect(function()
|
||||||
|
local newValue = getter()
|
||||||
|
local rawValue = Charm.toRaw(newValue)
|
||||||
|
local isReactive = newValue ~= rawValue
|
||||||
|
|
||||||
|
if not trackedState then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
trackedState.dispose = Charm.subscribe(getter, function(newValue, oldValue)
|
|
||||||
-- When preserving history, we need to keep track of previous values
|
-- When preserving history, we need to keep track of previous values
|
||||||
-- so we can create granular patches for each change.
|
-- so we can create granular patches for each change.
|
||||||
if config.preserveHistory then
|
if config.preserveHistory then
|
||||||
table.insert(globalUpdates, {
|
|
||||||
key = key,
|
|
||||||
newValue = newValue,
|
|
||||||
oldValue = oldValue,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
trackedState.currentValue = newValue
|
|
||||||
end)
|
|
||||||
|
|
||||||
trackedStates[key] = trackedState
|
|
||||||
end
|
|
||||||
|
|
||||||
local function trackReactive<T>(key: string, proxy: ReactiveProxy<T>)
|
|
||||||
if trackedStates[key] then
|
|
||||||
if trackedStates[key].target ~= proxy then
|
|
||||||
warn("[Charm] Attempted to sync different states with the same key:", key)
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local api = getmetatable(proxy)
|
|
||||||
local snapshot = utils.deepCopy(api.value)
|
|
||||||
local firstRun = true
|
|
||||||
|
|
||||||
local trackedState = {
|
|
||||||
target = proxy,
|
|
||||||
currentValue = api.value,
|
|
||||||
lastSyncedValue = snapshot,
|
|
||||||
isReactive = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
trackedState.dispose = Charm.effect(function()
|
|
||||||
api.track()
|
|
||||||
if not config.preserveHistory then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if firstRun then
|
|
||||||
firstRun = false
|
|
||||||
else
|
|
||||||
local oldSnapshot = snapshot
|
local oldSnapshot = snapshot
|
||||||
snapshot = utils.deepCopy(api.value)
|
snapshot = if isReactive then utils.deepCopy(rawValue) else newValue
|
||||||
table.insert(globalUpdates, {
|
table.insert(globalUpdates, {
|
||||||
key = key,
|
key = key,
|
||||||
newValue = snapshot,
|
newValue = snapshot,
|
||||||
oldValue = oldSnapshot,
|
oldValue = oldSnapshot,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trackedState.currentValue = rawValue
|
||||||
|
trackedState.isReactive = isReactive
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
trackedState = {
|
||||||
|
identifier = identifier,
|
||||||
|
currentValue = rawInitialValue,
|
||||||
|
lastSyncedValue = snapshot,
|
||||||
|
isReactive = initialIsReactive,
|
||||||
|
dispose = dispose,
|
||||||
|
}
|
||||||
|
|
||||||
trackedStates[key] = trackedState
|
trackedStates[key] = trackedState
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -269,9 +248,11 @@ local function addSignalsToClient(client: any, signals: { [string]: {} | () -> a
|
||||||
|
|
||||||
for key, signal in signals do
|
for key, signal in signals do
|
||||||
if type(signal) == "function" then
|
if type(signal) == "function" then
|
||||||
trackSignal(key, signal)
|
trackSignal(key, signal, signal)
|
||||||
else
|
else
|
||||||
trackReactive(key, signal :: any)
|
trackSignal(key, function()
|
||||||
|
return signal
|
||||||
|
end, signal)
|
||||||
end
|
end
|
||||||
|
|
||||||
if not clientKeys[key] then
|
if not clientKeys[key] then
|
||||||
|
|
|
||||||
9
packages/charm/src/index.d.ts
vendored
9
packages/charm/src/index.d.ts
vendored
|
|
@ -309,6 +309,15 @@ export function reactive<T extends object>(
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
): LuaTuple<[value: T, setter: (update: T | ((initialValue: T) => void)) => T]>;
|
): LuaTuple<[value: T, setter: (update: T | ((initialValue: T) => void)) => T]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the provided value is a reactive proxy created by `reactive()`.
|
||||||
|
*
|
||||||
|
* @param value A value to test.
|
||||||
|
* @returns `true` if the value is a reactive proxy, `false` otherwise.
|
||||||
|
* @see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
|
*/
|
||||||
|
export function isReactive(value: unknown): boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unwraps a reactive proxy to get the original table. If the provided value
|
* Unwraps a reactive proxy to get the original table. If the provided value
|
||||||
* is not a proxy, it is returned as-is.
|
* is not a proxy, it is returned as-is.
|
||||||
|
|
|
||||||
|
|
@ -923,6 +923,17 @@ local function reactive<T>(initialValue: T & {}): (T, (update: (T) -> () | T?) -
|
||||||
return createProxy(initialValue), proxyApi.set
|
return createProxy(initialValue), proxyApi.set
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Determines if the provided value is a reactive proxy created by `reactive()`.
|
||||||
|
|
||||||
|
@param value A value to test.
|
||||||
|
@return `true` if the value is a reactive proxy, `false` otherwise.
|
||||||
|
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
|
]]
|
||||||
|
local function isReactive(value: any): boolean
|
||||||
|
return type(value) == "table" and type(value.__target) == "table" and getmetatable(value) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Unwraps a reactive proxy to get the original table. If the provided value
|
Unwraps a reactive proxy to get the original table. If the provided value
|
||||||
is not a proxy, it is returned as-is.
|
is not a proxy, it is returned as-is.
|
||||||
|
|
@ -932,12 +943,9 @@ end
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
]]
|
]]
|
||||||
local function toRaw<T>(value: T): T
|
local function toRaw<T>(value: T): T
|
||||||
if type(value) == "table" and type(value.__target) == "table" then
|
if isReactive(value) then
|
||||||
-- Iteration starts tracking the reactive proxy
|
getmetatable(value :: ReactiveProxy<T>).track()
|
||||||
for _ in value do
|
return (value :: ReactiveProxy<T>).__target
|
||||||
break
|
|
||||||
end
|
|
||||||
return value.__target
|
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
@ -980,5 +988,6 @@ return {
|
||||||
mapper: (item: VI, key: KI) -> (VO, KO)
|
mapper: (item: VI, key: KI) -> (VO, KO)
|
||||||
) -> () -> { [KO]: VO }) & (<K, VI, VO>(getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }),
|
) -> () -> { [KO]: VO }) & (<K, VI, VO>(getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO }),
|
||||||
reactive = reactive,
|
reactive = reactive,
|
||||||
|
isReactive = isReactive,
|
||||||
toRaw = toRaw,
|
toRaw = toRaw,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue