mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: remove unnecessary features, update docs
This commit is contained in:
parent
7a88c784fa
commit
37f79ecc3f
20 changed files with 774 additions and 955 deletions
17
packages/charm-sync/src/index.d.ts
vendored
17
packages/charm-sync/src/index.d.ts
vendored
|
|
@ -141,7 +141,7 @@ export namespace client {
|
|||
*/
|
||||
export namespace server {
|
||||
/**
|
||||
* Subscribes a client to the given atoms. When an update occurs, the client
|
||||
* Subscribes a client to the given signals. When an update occurs, the client
|
||||
* will receive a partial state patch to merge into their local state. May be
|
||||
* called multiple times to subscribe to additional keys.
|
||||
*
|
||||
|
|
@ -150,9 +150,9 @@ export namespace server {
|
|||
* registering signals on the client should be done through `signalToAtom()`.
|
||||
*
|
||||
* @param client The client receiving state updates.
|
||||
* @param atoms A map of atoms to sync with the client.
|
||||
* @param signals A map of signals to sync with the client.
|
||||
*/
|
||||
export function subscribeClient(client: Player, atoms: { [key: string]: Atom<any> }): void;
|
||||
export function subscribeClient(client: Player, signals: { [key: string]: () => any }): void;
|
||||
|
||||
/**
|
||||
* Unsubscribes a client from receiving all state updates. To only unsubscribe
|
||||
|
|
@ -277,3 +277,14 @@ export function flattenAtoms<Atoms extends NestedAtomMap>(atoms: Atoms): Flatten
|
|||
* @returns `true` if the value is `None`, otherwise `false`.
|
||||
*/
|
||||
export function isNone(value: unknown): value is None;
|
||||
|
||||
/**
|
||||
* Converts a signal getter and setter into an atom. This is useful for
|
||||
* syncing client signals with the server, as the sync system requires both
|
||||
* the getter and setter to update state.
|
||||
*
|
||||
* @param getter A function that returns the current value of the signal.
|
||||
* @param setter A function that sets the value of the signal.
|
||||
* @returns An atom that can either get or set the signal's value.
|
||||
*/
|
||||
export function signalToAtom<T>(getter: () => T, setter: (value: ((prev: T) => T) | T) => T): Atom<T>;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ local globals = Charm.globals
|
|||
|
||||
type SyncStatus = number
|
||||
|
||||
type ClientAtoms = {
|
||||
type ClientKeys = {
|
||||
[string]: SyncStatus,
|
||||
}
|
||||
|
||||
|
|
@ -29,17 +29,17 @@ type StateUpdate = {
|
|||
local PENDING_INITIAL_STATE: SyncStatus = 0
|
||||
local LISTENING_FOR_CHANGES: SyncStatus = 1
|
||||
|
||||
local trackedAtoms: { [string]: TrackedState } = {}
|
||||
local trackedSignals: { [string]: TrackedState } = {}
|
||||
local globalUpdates: { StateUpdate } = {}
|
||||
local clients: { [any]: ClientAtoms } = {}
|
||||
local clients: { [any]: ClientKeys } = {}
|
||||
|
||||
local connection: RBXScriptConnection?
|
||||
local syncCallback: (client: any, payloads: { SyncPayload }) -> ()?
|
||||
|
||||
local function startTracking<T>(key: string, getter: () -> T)
|
||||
if trackedAtoms[key] then
|
||||
if trackedSignals[key] then
|
||||
-- Ignore duplicates, but warn against name collisions
|
||||
if trackedAtoms[key].getter ~= getter then
|
||||
if trackedSignals[key].getter ~= getter then
|
||||
warn("[Charm] Attempted to sync different states with the same key:", key)
|
||||
end
|
||||
return
|
||||
|
|
@ -68,27 +68,27 @@ local function startTracking<T>(key: string, getter: () -> T)
|
|||
trackedState.currentValue = newValue
|
||||
end)
|
||||
|
||||
trackedAtoms[key] = trackedState
|
||||
trackedSignals[key] = trackedState
|
||||
end
|
||||
|
||||
local function stopTracking(key: string)
|
||||
local tracker = trackedAtoms[key]
|
||||
local tracker = trackedSignals[key]
|
||||
if tracker then
|
||||
tracker.dispose()
|
||||
trackedAtoms[key] = nil
|
||||
trackedSignals[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function purgeUntracked()
|
||||
local active: { [string]: boolean } = {}
|
||||
|
||||
for _, clientAtoms in clients do
|
||||
for key in clientAtoms do
|
||||
for _, clientKeys in clients do
|
||||
for key in clientKeys do
|
||||
active[key] = true
|
||||
end
|
||||
end
|
||||
|
||||
for key in trackedAtoms do
|
||||
for key in trackedSignals do
|
||||
if not active[key] then
|
||||
stopTracking(key)
|
||||
end
|
||||
|
|
@ -107,9 +107,9 @@ local function diffGlobalUpdateBuffer(): { any }
|
|||
end
|
||||
|
||||
local function selectFromGlobalPatchList(client: any, globalPatches: { any }): { SyncPayload }
|
||||
local clientAtoms = clients[client]
|
||||
local clientKeys = clients[client]
|
||||
|
||||
if not clientAtoms then
|
||||
if not clientKeys then
|
||||
return {}
|
||||
end
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
|
|||
end
|
||||
|
||||
local key = update.key
|
||||
local status = clientAtoms[key]
|
||||
local status = clientKeys[key]
|
||||
|
||||
if status == LISTENING_FOR_CHANGES then
|
||||
table.insert(payloads, {
|
||||
|
|
@ -137,20 +137,20 @@ local function selectFromGlobalPatchList(client: any, globalPatches: { any }): {
|
|||
type = "init",
|
||||
data = { [key] = update.currentValue },
|
||||
})
|
||||
clientAtoms[key] = LISTENING_FOR_CHANGES
|
||||
clientKeys[key] = LISTENING_FOR_CHANGES
|
||||
end
|
||||
end
|
||||
|
||||
-- If a pending atom was never updated, we still need to send its initial
|
||||
-- state. This can happen if the atom was registered after its last change.
|
||||
-- If a pending signal was never updated, we still need to send its initial
|
||||
-- state. This can happen if a signal was registered after its last change.
|
||||
local initData: { [string]: any } = {}
|
||||
|
||||
for key, status in clientAtoms do
|
||||
local trackedState = trackedAtoms[key]
|
||||
for key, status in clientKeys do
|
||||
local trackedState = trackedSignals[key]
|
||||
|
||||
if status == PENDING_INITIAL_STATE and trackedState then
|
||||
initData[key] = trackedState.currentValue
|
||||
clientAtoms[key] = LISTENING_FOR_CHANGES
|
||||
clientKeys[key] = LISTENING_FOR_CHANGES
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ local function diffGlobalState(): any
|
|||
local currentGlobalState: { [string]: any } = {}
|
||||
local previousGlobalState: { [string]: any } = {}
|
||||
|
||||
for key, tracker in trackedAtoms do
|
||||
for key, tracker in trackedSignals do
|
||||
currentGlobalState[key] = tracker.currentValue
|
||||
previousGlobalState[key] = tracker.previousValue
|
||||
tracker.previousValue = tracker.currentValue
|
||||
|
|
@ -175,9 +175,9 @@ local function diffGlobalState(): any
|
|||
end
|
||||
|
||||
local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPayload }
|
||||
local clientAtoms = clients[client]
|
||||
local clientKeys = clients[client]
|
||||
|
||||
if not clientAtoms then
|
||||
if not clientKeys then
|
||||
return {}
|
||||
end
|
||||
|
||||
|
|
@ -185,16 +185,16 @@ local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPaylo
|
|||
local initData: { [string]: any } = {}
|
||||
local patchData: { [string]: any } = {}
|
||||
|
||||
-- Create patches for initialized atoms that changed, and add atoms that
|
||||
-- are still pending initial state to the init payload.
|
||||
for key, status in clientAtoms do
|
||||
local trackedState = trackedAtoms[key]
|
||||
-- Create patches for initialized signals that changed, and add signals
|
||||
-- that are still pending initial state to the init payload.
|
||||
for key, status in clientKeys do
|
||||
local trackedState = trackedSignals[key]
|
||||
|
||||
if status == LISTENING_FOR_CHANGES then
|
||||
patchData[key] = globalPatch[key]
|
||||
elseif status == PENDING_INITIAL_STATE then
|
||||
initData[key] = trackedState.currentValue
|
||||
clientAtoms[key] = LISTENING_FOR_CHANGES
|
||||
clientKeys[key] = LISTENING_FOR_CHANGES
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -209,7 +209,7 @@ local function selectFromGlobalPatch(client: any, globalPatch: any): { SyncPaylo
|
|||
return payloads
|
||||
end
|
||||
|
||||
--- Subscribes a client to the given atoms. When an update occurs, the client
|
||||
--- Subscribes a client to the given signals. When an update occurs, the client
|
||||
--- will receive a partial state patch to merge into their local state. May be
|
||||
--- called multiple times to subscribe to additional keys.
|
||||
---
|
||||
|
|
@ -218,21 +218,21 @@ end
|
|||
--- registering signals on the client should be done through `signalToAtom()`.
|
||||
---
|
||||
--- @param client The client receiving state updates.
|
||||
--- @param atoms A map of atoms to sync with the client.
|
||||
local function subscribeClient(client: any, atoms: { [string]: () -> any })
|
||||
--- @param signals A map of signals to sync with the client.
|
||||
local function subscribeClient(client: any, signals: { [string]: () -> any })
|
||||
if not clients[client] then
|
||||
clients[client] = {}
|
||||
end
|
||||
|
||||
local clientAtoms = clients[client]
|
||||
local clientKeys = clients[client]
|
||||
|
||||
for key, state in atoms do
|
||||
for key, state in signals do
|
||||
startTracking(key, if type(state) == "function" then state else state.get)
|
||||
|
||||
if not clientAtoms[key] then
|
||||
-- Mark the atom as pending initial state so we send its full value
|
||||
-- the next time we sync.
|
||||
clientAtoms[key] = PENDING_INITIAL_STATE
|
||||
if not clientKeys[key] then
|
||||
-- Mark the signal as pending initial state so we send its full
|
||||
-- value the next time we sync.
|
||||
clientKeys[key] = PENDING_INITIAL_STATE
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -250,18 +250,18 @@ end
|
|||
--- @param client The client receiving state updates.
|
||||
--- @param keys The keys of the state to unsubscribe from.
|
||||
local function unsubscribeClientFrom(client: any, ...: string)
|
||||
local clientAtoms = clients[client]
|
||||
local clientKeys = clients[client]
|
||||
|
||||
if not clientAtoms then
|
||||
if not clientKeys then
|
||||
return
|
||||
end
|
||||
|
||||
for index = 1, select("#", ...) do
|
||||
local key = select(index, ...)
|
||||
clientAtoms[key] = nil
|
||||
clientKeys[key] = nil
|
||||
end
|
||||
|
||||
if next(clientAtoms) == nil then
|
||||
if next(clientKeys) == nil then
|
||||
clients[client] = nil
|
||||
end
|
||||
end
|
||||
|
|
@ -346,7 +346,7 @@ end
|
|||
--- Unsubscribes all clients, stops tracking all state, and disconnects
|
||||
--- from the automatic sync interval.
|
||||
local function reset()
|
||||
table.clear(trackedAtoms)
|
||||
table.clear(trackedSignals)
|
||||
table.clear(clients)
|
||||
table.clear(globalUpdates)
|
||||
disconnect()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ local function flattenState(states: NestedState): FlattenedState
|
|||
return result
|
||||
end
|
||||
|
||||
--- Converts a signal getter and setter into an atom. This is useful for
|
||||
--- syncing client signals with the server, as the sync system requires both
|
||||
--- the getter and setter to update state.
|
||||
---
|
||||
--- @param getter A function that returns the current value of the signal.
|
||||
--- @param setter A function that sets the value of the signal.
|
||||
--- @return An atom that can either get or set the signal's value.
|
||||
local function signalToAtom<T>(getter: () -> T, setter: (value: ((T) -> T) | T) -> T): Atom<T>
|
||||
return function(...)
|
||||
return if select("#", ...) == 0 then getter() else setter((...))
|
||||
|
|
|
|||
|
|
@ -354,25 +354,4 @@ test("should run cleanups outside any evaluation context", function()
|
|||
assert(calls == 0, "should not run after b update")
|
||||
end)
|
||||
|
||||
test("should allow self cleanup", function()
|
||||
local calls = 0
|
||||
local cleanups = 0
|
||||
local a, setA = signal(0)
|
||||
|
||||
effect(function(stopEffect)
|
||||
onCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
stopEffect()
|
||||
a()
|
||||
calls += 1
|
||||
end)
|
||||
|
||||
assert(calls == 1, "should call effect once")
|
||||
assert(cleanups == 1, "should call cleanup once")
|
||||
setA(1)
|
||||
assert(calls == 1, "should not call effect after a update")
|
||||
assert(cleanups == 1, "should not call cleanup after a update")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ local suite = require("../../../../test/suite")
|
|||
local signal = Charm.signal
|
||||
local listen = Charm.listen
|
||||
local effect = Charm.effect
|
||||
local onCleanup = Charm.onCleanup
|
||||
local test = suite.test
|
||||
|
||||
test("should listen to selector", function()
|
||||
|
|
@ -116,24 +115,4 @@ test("should skip redundant updates", function()
|
|||
assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`)
|
||||
end)
|
||||
|
||||
test("should allow self cleanup", function()
|
||||
local calls = 0
|
||||
local cleanups = 0
|
||||
local a, setA = signal(0)
|
||||
|
||||
listen(a, function(_, _, stopEffect)
|
||||
onCleanup(function()
|
||||
cleanups += 1
|
||||
end)
|
||||
stopEffect()
|
||||
calls += 1
|
||||
end)
|
||||
|
||||
assert(calls == 1, "should call effect once")
|
||||
assert(cleanups == 1, "should call cleanup once")
|
||||
setA(1)
|
||||
assert(calls == 1, "should not call effect after a update")
|
||||
assert(cleanups == 1, "should not call cleanup after a update")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,8 @@ type EffectScope = ReactiveNode & {
|
|||
}
|
||||
|
||||
type Effect = ReactiveNode & {
|
||||
fn: (stop: () -> ()) -> (() -> ())?,
|
||||
fn: () -> (() -> ())?,
|
||||
cleanups: { () -> () },
|
||||
stop: () -> (),
|
||||
}
|
||||
|
||||
type Computed<T = any> = ReactiveNode & {
|
||||
|
|
@ -117,7 +116,7 @@ local function run(e: Effect): ()
|
|||
e.depsTail = nil
|
||||
e.flags = bit32.bor(WATCHING, RECURSED_CHECK)
|
||||
local prevSub = setActiveSub(e)
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(e.fn, e.stop)
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(e.fn)
|
||||
activeSub = prevSub
|
||||
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
|
||||
purgeDeps(e)
|
||||
|
|
@ -197,7 +196,7 @@ local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
|
|||
end
|
||||
|
||||
local function computedOper<T>(this: Computed<T>): () -> T
|
||||
return function(): T
|
||||
local function computedGetter(): T
|
||||
local flags = this.flags
|
||||
if
|
||||
bit32.btest(flags, DIRTY)
|
||||
|
|
@ -225,13 +224,14 @@ local function computedOper<T>(this: Computed<T>): () -> T
|
|||
end
|
||||
return this.value :: T
|
||||
end
|
||||
return computedGetter
|
||||
end
|
||||
|
||||
local function signalOper<T>(
|
||||
this: Signal<T>,
|
||||
equals: ((current: T, incoming: T) -> boolean)?
|
||||
): (() -> T, (((T) -> T) | T) -> T)
|
||||
local function setter(update: ((T) -> T) | T): T
|
||||
local function signalSetter(update: ((T) -> T) | T): T
|
||||
local value: T
|
||||
if type(update) == "function" then
|
||||
local prevSub = setActiveSub(nil)
|
||||
|
|
@ -262,7 +262,7 @@ local function signalOper<T>(
|
|||
return this.currentValue
|
||||
end
|
||||
|
||||
local function getter(): T
|
||||
local function signalGetter(): T
|
||||
if bit32.btest(this.flags, DIRTY) then
|
||||
if updateSignal(this) then
|
||||
local subs = this.subs
|
||||
|
|
@ -285,7 +285,7 @@ local function signalOper<T>(
|
|||
return this.currentValue
|
||||
end
|
||||
|
||||
return getter, setter
|
||||
return signalGetter, signalSetter
|
||||
end
|
||||
|
||||
local function effectScopeOper(this: EffectScope): () -> ()
|
||||
|
|
@ -311,7 +311,7 @@ end
|
|||
--- @param initialValue The initial value of the signal.
|
||||
--- @param equals A comparator function to determine if the signal's value has changed.
|
||||
--- @return A tuple containing the getter and setter functions for the signal.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#signalstate-equals
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
|
||||
local function signal<T>(
|
||||
initialValue: T,
|
||||
equals: ((current: T, incoming: T) -> boolean)?
|
||||
|
|
@ -326,30 +326,24 @@ local function signal<T>(
|
|||
}, equals)
|
||||
end
|
||||
|
||||
--- Creates a reactive atom that acts as both a getter and setter. Calling the
|
||||
--- atom with an argument sets its value, while calling it with no arguments
|
||||
--- returns its current value.
|
||||
--- Creates a reactive atom that acts as both a signal getter and setter.
|
||||
--- Calling the atom with an argument sets its value, while calling it with no
|
||||
--- arguments returns its current value.
|
||||
---
|
||||
--- @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
|
||||
--- @param initialValue The initial value of the atom. If empty, the atom's value will be `nil`.
|
||||
--- @param equals A comparator function to determine if the atom's value has changed.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-equals
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-equals
|
||||
local function atom<T>(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): (...((T) -> T) | T) -> T
|
||||
local getter, setter = signal(initialValue, equals)
|
||||
return function(...)
|
||||
local function atomGetterSetter(...)
|
||||
return if select("#", ...) == 0 then getter() else setter((...))
|
||||
end
|
||||
return atomGetterSetter
|
||||
end
|
||||
|
||||
--- Creates a new atom that is computed based on the values of other atoms.
|
||||
--- The returned atom is read-only, and its value is automatically updated when
|
||||
--- any atoms accessed from within the getter function change.
|
||||
---
|
||||
--- @example
|
||||
--- ```ts
|
||||
--- const count = atom(1);
|
||||
--- const plusOne = computed(() => count() + 1);
|
||||
--- print(plusOne()); // 2
|
||||
--- ```
|
||||
--- Creates a new read-only signal that is computed based on the values of
|
||||
--- other signals. The computed signal's value is automatically updated when a
|
||||
--- change is detected in any of its dependencies.
|
||||
---
|
||||
--- @param getter A function that produces the next value.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
||||
|
|
@ -360,50 +354,41 @@ local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
|||
})
|
||||
end
|
||||
|
||||
--- Creates an effect that runs a callback in response to atom state changes.
|
||||
--- An effect tracks which atoms are accessed during its execution, and re-runs
|
||||
--- the callback when those atoms change.
|
||||
--- Creates an effect that runs a callback in response to signal state changes.
|
||||
--- An effect tracks which signals are accessed during its execution, and runs
|
||||
--- the callback when those signals change.
|
||||
---
|
||||
--- The effect callback may return a cleanup function, which gets called once,
|
||||
--- either before the effect re-runs or when it is disposed.
|
||||
---
|
||||
--- If the effect is called within another effect, it will be disposed when the
|
||||
--- outer effect re-runs.
|
||||
--- parent effect re-runs.
|
||||
---
|
||||
--- @param fn The function to run when dependencies change.
|
||||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||
local function effect(fn: (stop: () -> ()) -> ()): () -> ()
|
||||
local function effect(fn: () -> ()): () -> ()
|
||||
local e: Effect = {
|
||||
fn = fn,
|
||||
flags = WATCHING,
|
||||
cleanups = {},
|
||||
stop = nil :: any,
|
||||
}
|
||||
e.stop = effectOper(e)
|
||||
local prevSub = setActiveSub(e)
|
||||
if prevSub and (globals.trackInnerEffects or prevSub.flags == NONE) then
|
||||
link(e, prevSub, 0)
|
||||
end
|
||||
local stopped = false
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(fn, function()
|
||||
stopped = true
|
||||
end)
|
||||
local success, result: (() -> ())? = (if globals.strict then noYield else pcall)(fn)
|
||||
activeSub = prevSub
|
||||
if not success then
|
||||
error(result)
|
||||
elseif result then
|
||||
table.insert(e.cleanups, result)
|
||||
end
|
||||
if stopped then
|
||||
e.stop()
|
||||
end
|
||||
return e.stop
|
||||
return effectOper(e)
|
||||
end
|
||||
|
||||
--- Creates an effect scope that can capture multiple reactive effects (i.e.
|
||||
--- computed, effect, subscribe) created within it so that they can be disposed
|
||||
--- together.
|
||||
--- Creates an effect scope that can capture reactive effects and computed
|
||||
--- signals created within it so that they can be disposed together.
|
||||
---
|
||||
--- @param fn A function that may create reactive effects.
|
||||
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
||||
|
|
@ -428,12 +413,12 @@ local function effectScope(fn: () -> (), detached: boolean?): () -> ()
|
|||
return effectScopeOper(e)
|
||||
end
|
||||
|
||||
--- Registers a cleanup function to the current active effect or effect scope.
|
||||
--- If there is no active effect or scope, a warning is logged and the callback
|
||||
--- is not registered, unless `failSilently` is set to true.
|
||||
--- Binds the cleanup function to the current active effect or effect scope. If
|
||||
--- there is no active effect, a warning is logged, unless `failSilently` is
|
||||
--- set to `true`.
|
||||
---
|
||||
--- @param callback The cleanup function to register.
|
||||
--- @param failSilently If true, suppresses the warning when there is no active effect or scope. Defaults to false.
|
||||
--- @param failSilently If `true`, suppresses the warning when there is no active effect or scope. Defaults to `false`.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
||||
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
||||
if activeSub and (activeSub :: Effect).cleanups then
|
||||
|
|
@ -443,7 +428,7 @@ local function onCleanup(cleanup: () -> (), failSilently: boolean?)
|
|||
end
|
||||
end
|
||||
|
||||
--- Runs the callback without subscribing to atom updates or capturing inner
|
||||
--- Runs the callback without subscribing to signal updates or capturing inner
|
||||
--- effects in the parent effect or scope.
|
||||
---
|
||||
--- For an alternative that still cleans up inner effects, use `peek` instead.
|
||||
|
|
@ -462,7 +447,7 @@ local function untracked<Args..., Results...>(callback: (Args...) -> Results...,
|
|||
return unpack(results, 2)
|
||||
end
|
||||
|
||||
--- Runs the callback without subscribing to atom updates, but still captures
|
||||
--- Runs the callback without subscribing to signal updates, but still captures
|
||||
--- inner effects so that they dispose with the parent effect.
|
||||
---
|
||||
--- To avoid capturing inner effects, use `untracked` instead.
|
||||
|
|
@ -489,10 +474,10 @@ local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...:
|
|||
end
|
||||
end
|
||||
|
||||
--- Combines multiple atom updates into a single "commit" that runs effects at
|
||||
--- the end of the provided callback.
|
||||
--- Combines multiple updates into a single "commit" that runs effects and
|
||||
--- computed signals after the provided callback finishes running.
|
||||
---
|
||||
--- @param callback A function that performs multiple atom updates.
|
||||
--- @param callback A function that performs multiple updates.
|
||||
--- @param args Arguments to pass to the callback.
|
||||
--- @return The return value of the callback.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
|
||||
|
|
@ -506,18 +491,56 @@ local function batched<Args..., Results...>(callback: (Args...) -> Results..., .
|
|||
return unpack(results, 2)
|
||||
end
|
||||
|
||||
--- Converts a non-signal into a computed signal. If the callback is already a
|
||||
--- signal, it is returned as-is. Useful for memoizing a getter function
|
||||
--- without creating unnecessary nested signals.
|
||||
---
|
||||
--- @param getter A function that returns the value to track.
|
||||
--- @return A computed signal that tracks the value returned by the getter.
|
||||
local function toComputed<T>(getter: () -> T): () -> T
|
||||
local name = debug.info(getter, "n")
|
||||
if name == "signalGetter" or name == "computedGetter" or name == "atomGetterSetter" then
|
||||
return getter
|
||||
end
|
||||
return computed(function()
|
||||
return getter()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Creates an effect that runs the callback one immediately, and then again
|
||||
--- when the value returned by the getter changes. The callback receives the new
|
||||
--- value and the previous value as arguments.
|
||||
---
|
||||
--- Effects created within the callback are cleaned up when the listener runs
|
||||
--- or the listener is disposed.
|
||||
---
|
||||
--- @param getter A function that returns the value to watch.
|
||||
--- @param callback The function to run when the value changes.
|
||||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?) -> ()): () -> ()
|
||||
local tracked = toComputed(getter)
|
||||
local current: T
|
||||
return effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
peek(callback, current, previous)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Creates an effect that only runs the callback when the value returned by the
|
||||
--- getter changes. The callback receives the new value and the previous value
|
||||
--- as arguments.
|
||||
---
|
||||
--- Effects created within the callback are cleaned up when the listener runs
|
||||
--- or the listener is disposed.
|
||||
---
|
||||
--- @param getter A function that returns the value to watch.
|
||||
--- @param callback The function to run when the value changes.
|
||||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
||||
local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T) -> ()): () -> ()
|
||||
local tracked = computed(function()
|
||||
return getter()
|
||||
end)
|
||||
local tracked = toComputed(getter)
|
||||
local current: T
|
||||
local mounted = true
|
||||
return effect(function()
|
||||
|
|
@ -531,34 +554,6 @@ local function subscribe<T>(getter: () -> T, callback: (state: T, lastState: T)
|
|||
end)
|
||||
end
|
||||
|
||||
--- Creates an effect that runs the callback one immediately, and then again
|
||||
--- when the value returned by the getter changes. The callback receives the new
|
||||
--- value, the previous value, and a cleanup function as arguments.
|
||||
---
|
||||
--- @param getter A function that returns the value to watch.
|
||||
--- @param callback The function to run when the value changes.
|
||||
--- @return A function for disposing the effect.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||
local function listen<T>(getter: () -> T, callback: (state: T, previous: T?, dispose: () -> ()) -> ()): () -> ()
|
||||
local tracked = computed(function()
|
||||
return getter()
|
||||
end)
|
||||
local current: T
|
||||
local disposed = false
|
||||
local dispose = function()
|
||||
disposed = true
|
||||
end
|
||||
dispose = effect(function()
|
||||
local previous = current
|
||||
current = tracked()
|
||||
peek(callback, current, previous, dispose)
|
||||
end)
|
||||
if disposed then
|
||||
dispose()
|
||||
end
|
||||
return dispose
|
||||
end
|
||||
|
||||
local reactiveSystem = createReactiveSystem({
|
||||
update = function(node)
|
||||
if node.depsTail then
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ local isO2 = inlineTest() ~= "inlineTest"
|
|||
local isDev = not isO2 or _G.__DEV__ == true
|
||||
|
||||
return {
|
||||
--- Enforces synchronous, non-yielding behavior in atoms and effects.
|
||||
--- Enforces synchronous, non-yielding behavior in signals and effects.
|
||||
--- Also enables state validation in Charm Sync to catch sync errors early.
|
||||
--- Enabled if the optimization level is below 2, which is true in studio.
|
||||
strict = isDev,
|
||||
--- Enforces immutability of atoms by deep-freezing table values. Enabled
|
||||
--- Enforces immutability of tables by deep-freezing table values. Enabled
|
||||
--- if the optimization level is below 2, which is true in studio.
|
||||
frozen = isDev,
|
||||
--- Whether parent effects should track inner effects and clean them up
|
||||
|
|
|
|||
223
packages/charm/src/index.d.ts
vendored
223
packages/charm/src/index.d.ts
vendored
|
|
@ -20,13 +20,13 @@ export type Equals<T> = (current: T, incoming: T) => boolean;
|
|||
*/
|
||||
export const globals: {
|
||||
/**
|
||||
* Enforces synchronous, non-yielding behavior in atoms and effects.
|
||||
* Enforces synchronous, non-yielding behavior in signals and effects.
|
||||
* Also enables state validation in Charm Sync to catch sync errors early.
|
||||
* Enabled if the optimization level is below 2, which is true in studio.
|
||||
*/
|
||||
strict: boolean;
|
||||
/**
|
||||
* Enforces immutability of atoms by deep-freezing table values. Enabled
|
||||
* Enforces immutability of tables by deep-freezing table values. Enabled
|
||||
* if the optimization level is below 2, which is true in studio.
|
||||
*/
|
||||
frozen: boolean;
|
||||
|
|
@ -44,84 +44,138 @@ export const globals: {
|
|||
*
|
||||
* @param initialValue The initial value of the signal.
|
||||
* @param equals A comparator function to determine if the signal's value has changed.
|
||||
* @return A tuple containing the getter and setter functions for the signal.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#signalstate-equals
|
||||
* @returns A tuple containing the getter and setter functions for the signal.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
|
||||
*/
|
||||
export function signal<T>(
|
||||
value: T,
|
||||
initialValue: T,
|
||||
equals?: Equals<T>,
|
||||
): LuaTuple<[() => T, (newValue: T | ((currentValue: T) => T)) => T]>;
|
||||
): LuaTuple<[getter: () => T, setter: (newValue: T | ((currentValue: T) => T)) => T]>;
|
||||
export function signal<T>(): LuaTuple<
|
||||
[() => T | undefined, (newValue: T | ((currentValue?: T) => T | undefined)) => T | undefined]
|
||||
[getter: () => T | undefined, setter: (newValue: T | ((currentValue?: T) => T | undefined)) => T | undefined]
|
||||
>;
|
||||
|
||||
/**
|
||||
* Creates a reactive atom that acts as both a getter and setter. Calling the
|
||||
* atom with an argument sets its value, while calling it with no arguments
|
||||
* returns its current value.
|
||||
* Creates a reactive atom that acts as both a signal getter and setter.
|
||||
* Calling the atom with an argument sets its value, while calling it with no
|
||||
* arguments returns its current value.
|
||||
*
|
||||
* @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
|
||||
* @param initialValue The initial value of the atom. If empty, the atom's value will be `nil`.
|
||||
* @param equals A comparator function to determine if the atom's value has changed.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-equals
|
||||
*/
|
||||
export function atom<T>(value: T, equals?: Equals<T>): Atom<T>;
|
||||
export function atom<T>(initialValue: T, equals?: Equals<T>): Atom<T>;
|
||||
export function atom<T>(): Atom<T | undefined>;
|
||||
|
||||
/**
|
||||
* Creates a new atom that is computed based on the values of other atoms.
|
||||
* The returned atom is read-only, and its value is automatically updated when
|
||||
* any atoms accessed from within the getter function change.
|
||||
*
|
||||
* If an initial value is provided, the getter will receive this value as its
|
||||
* first argument on the initial run, instead of `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const count = atom(1);
|
||||
* const plusOne = computed(() => count() + 1);
|
||||
* print(plusOne()); // 2
|
||||
* ```
|
||||
* Creates a new read-only signal that is computed based on the values of
|
||||
* other signals. The computed signal's value is automatically updated when a
|
||||
* change is detected in any of its dependencies.
|
||||
*
|
||||
* @param getter A function that produces the next value.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
||||
*/
|
||||
export function computed<T>(getter: (previousValue?: T) => T): () => T;
|
||||
|
||||
/**
|
||||
* Creates an effect that runs a callback in response to atom state changes.
|
||||
* An effect tracks which atoms are accessed during its execution, and re-runs
|
||||
* the callback when those atoms change.
|
||||
* Creates an effect that runs a callback in response to signal state changes.
|
||||
* An effect tracks which signals are accessed during its execution, and runs
|
||||
* the callback when those signals change.
|
||||
*
|
||||
* The effect callback may return a cleanup function, which gets called once,
|
||||
* either before the effect re-runs or when it is disposed.
|
||||
*
|
||||
* If the effect is called within another effect, it will be disposed when the
|
||||
* outer effect re-runs.
|
||||
* parent effect re-runs.
|
||||
*
|
||||
* @param callback The function to run when dependencies change.
|
||||
* @param fn The function to run when dependencies change.
|
||||
* @returns A function for disposing the effect.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||
*/
|
||||
export function effect(callback: (dispose: Cleanup) => Cleanup | void): Cleanup;
|
||||
export function effect(callback: () => Cleanup | void): Cleanup;
|
||||
|
||||
/**
|
||||
* Creates an effect scope that can capture multiple reactive effects (i.e.
|
||||
* computed, effect, subscribe) created within it so that they can be disposed
|
||||
* together.
|
||||
* Creates an effect scope that can capture reactive effects and computed
|
||||
* signals created within it so that they can be disposed together.
|
||||
*
|
||||
* @param callback A function that may create reactive effects.
|
||||
* @param detached If true, the scope will not be automatically disposed of
|
||||
* when the parent scope is disposed. Defaults to false.
|
||||
* @param fn A function that may create reactive effects.
|
||||
* @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
||||
* @returns A function for disposing all effects created within the scope.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
||||
*/
|
||||
export function effectScope(callback: () => Cleanup | void, detached?: boolean): Cleanup;
|
||||
|
||||
/**
|
||||
* Binds the cleanup function to the current active effect or effect scope. If
|
||||
* there is no active effect, a warning is logged, unless `failSilently` is
|
||||
* set to `true`.
|
||||
*
|
||||
* @param callback The cleanup function to register.
|
||||
* @param failSilently If `true`, suppresses the warning when there is no active effect or scope. Defaults to `false`.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
||||
*/
|
||||
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
||||
|
||||
/**
|
||||
* Runs the callback without subscribing to signal updates or capturing inner
|
||||
* effects in the parent effect or scope.
|
||||
*
|
||||
* For an alternative that still cleans up inner effects, use `peek` instead.
|
||||
*
|
||||
* @param callback The callback function.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
|
||||
*/
|
||||
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Runs the callback without subscribing to signal updates, but still captures
|
||||
* inner effects so that they dispose with the parent effect.
|
||||
*
|
||||
* To avoid capturing inner effects, use `untracked` instead.
|
||||
*
|
||||
* @param callback The callback function.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
||||
*/
|
||||
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Combines multiple updates into a single "commit" that runs effects and
|
||||
* computed signals after the provided callback finishes running.
|
||||
*
|
||||
* @param callback A function that performs multiple updates.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#batchedcallback
|
||||
*/
|
||||
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Creates an effect that runs the callback one immediately, and then again
|
||||
* when the value returned by the getter changes. The callback receives the new
|
||||
* value and the previous value as arguments.
|
||||
*
|
||||
* Effects created within the callback are cleaned up when the listener runs
|
||||
* or the listener is disposed.
|
||||
*
|
||||
* @param getter A function that returns the value to watch.
|
||||
* @param callback The function to run when the value changes.
|
||||
* @returns A function for disposing the effect.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||
*/
|
||||
export function listen<T>(getter: () => T, callback: (value: T, previousValue: T | undefined) => void): Cleanup;
|
||||
|
||||
/**
|
||||
* Creates an effect that only runs the callback when the value returned by the
|
||||
* getter changes. The callback receives the new value and the previous value
|
||||
* as arguments.
|
||||
*
|
||||
* Effects created within the callback are cleaned up when the listener runs
|
||||
* or the listener is disposed.
|
||||
*
|
||||
* @param getter A function that returns the value to watch.
|
||||
* @param callback The function to run when the value changes.
|
||||
* @returns A function for disposing the effect.
|
||||
|
|
@ -130,28 +184,12 @@ export function effectScope(callback: () => Cleanup | void, detached?: boolean):
|
|||
export function subscribe<T>(getter: () => T, callback: (value: T, previousValue: T) => void): Cleanup;
|
||||
|
||||
/**
|
||||
* Creates an effect that runs the callback one immediately, and then again
|
||||
* when the value returned by the getter changes. The callback receives the new
|
||||
* value, the previous value, and a cleanup function as arguments.
|
||||
* Creates a new read-only signal that is computed by mapping over the entries
|
||||
* of an existing atom. If the key is omitted from the mapper's return value,
|
||||
* the original key is preserved.
|
||||
*
|
||||
* @param getter A function that returns the value to watch.
|
||||
* @param callback The function to run when the value changes.
|
||||
* @returns A function for disposing the effect.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||
*/
|
||||
export function listen<T>(
|
||||
getter: () => T,
|
||||
callback: (value: T, previousValue: T | undefined, dispose: Cleanup) => void,
|
||||
): Cleanup;
|
||||
|
||||
/**
|
||||
* Creates a new atom that is computed by mapping over the entries of an
|
||||
* existing atom. The returned atom is read-only, and its value is
|
||||
* automatically updated when the source atom changes.
|
||||
*
|
||||
* @param getter A function that returns the table to map over.
|
||||
* @param mapper A function that transforms each value-key pair from the source
|
||||
* into a new value or value-key pair for the resulting table.
|
||||
* @param getter Returns the table to map over.
|
||||
* @param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
||||
*/
|
||||
export function mapped<VI, KI, VO, KO = KI>(
|
||||
|
|
@ -170,70 +208,27 @@ export function mapped<VI, KI extends Key, VO, KO extends Key = KI>(
|
|||
): () => { readonly [K in KO]: VO };
|
||||
|
||||
/**
|
||||
* Calls the observer function for each unique key in the table returned by the
|
||||
* getter. The observer is called once for each key, and again when a new key
|
||||
* Calls the callback function for each unique key in the table returned by the
|
||||
* getter. The callback is called once for each key, and again when a new key
|
||||
* is added.
|
||||
*
|
||||
* The observer may return a cleanup function, which will be called when the
|
||||
* key is removed or when the entire observer is disposed.
|
||||
* The callback may return a cleanup function, which will be called when the
|
||||
* key is removed or when the entire callback is disposed.
|
||||
*
|
||||
* Effects created within the callback are cleaned up when the item is removed
|
||||
* from the table or when the callback is disposed.
|
||||
*
|
||||
* @param getter A function that returns the table to observe.
|
||||
* @param observer A function that is called for each key in the table.
|
||||
* @param callback A function that is called for each key in the table.
|
||||
* @returns A function for disposing the observer.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-observer
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
||||
*/
|
||||
export function observe<V, K = number>(
|
||||
getter: () => Map<K, V> | ReadonlyMap<K, V> | readonly V[],
|
||||
observer: (value: V, key: K) => Cleanup | void,
|
||||
callback: (value: V, key: K) => Cleanup | void,
|
||||
): Cleanup;
|
||||
// Overload for objects
|
||||
export function observe<V, K extends Key>(
|
||||
getter: () => { readonly [K in Key]: V },
|
||||
observer: (value: V, key: K) => Cleanup | void,
|
||||
callback: (value: V, key: K) => Cleanup | void,
|
||||
): Cleanup;
|
||||
|
||||
/**
|
||||
* Combines multiple atom updates into a single "commit" that runs effects at
|
||||
* the end of the provided callback.
|
||||
*
|
||||
* @param callback A function that performs multiple atom updates.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
*/
|
||||
export function batched<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Runs the callback without subscribing to atom updates or capturing inner
|
||||
* effects in the parent effect or scope.
|
||||
*
|
||||
* For an alternative that still cleans up inner effects, use `peek` instead.
|
||||
*
|
||||
* @param callback The callback function.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
*/
|
||||
export function untracked<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Runs the callback without subscribing to atom updates, but still captures
|
||||
* inner effects so that they dispose with the parent effect.
|
||||
*
|
||||
* To avoid capturing inner effects, use `untracked` instead.
|
||||
*
|
||||
* @param callback The callback function.
|
||||
* @param args Arguments to pass to the callback.
|
||||
* @returns The return value of the callback.
|
||||
*/
|
||||
export function peek<Args extends any[], Result>(callback: (...args: Args) => Result, ...args: Args): Result;
|
||||
|
||||
/**
|
||||
* Registers a cleanup function to the current active effect or effect scope.
|
||||
* If there is no active effect or scope, a warning is logged and the callback
|
||||
* is not registered, unless `failSilently` is set to true.
|
||||
*
|
||||
* @param callback The cleanup function to register.
|
||||
* @param failSilently If true, suppresses the warning when there is no active
|
||||
* effect or scope. Defaults to false.
|
||||
* @see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
||||
*/
|
||||
export function onCleanup(callback: Cleanup, failSilently?: boolean): void;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ type Mapped =
|
|||
& (<K, VI, VO>(getter: () -> { [K]: VI }, mapper: (item: VI, key: K) -> VO) -> () -> { [K]: VO })
|
||||
|
||||
local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
||||
local currentData: { [KI]: VI } = {}
|
||||
local currentInput: { [KI]: VI } = {}
|
||||
local keyCache: { [KI]: KO } = {}
|
||||
|
||||
local function map(value: VI, key: KI): (VO, KO)
|
||||
|
|
@ -25,38 +25,38 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
|
|||
|
||||
return computed(function(previousOutput)
|
||||
local currentOutput: { [KO]: VO } = previousOutput or {}
|
||||
local previousData = currentData
|
||||
currentData = getter()
|
||||
local previousInput = currentInput
|
||||
currentInput = getter()
|
||||
|
||||
for key in previousData do
|
||||
if currentData[key] ~= nil then
|
||||
for keyIn in previousInput do
|
||||
if currentInput[keyIn] ~= nil then
|
||||
continue
|
||||
end
|
||||
|
||||
local cachedKey = keyCache[key]
|
||||
local prevKeyOut = keyCache[keyIn]
|
||||
|
||||
if cachedKey then
|
||||
if prevKeyOut then
|
||||
if currentOutput == previousOutput then
|
||||
currentOutput = table.clone(currentOutput)
|
||||
end
|
||||
currentOutput[cachedKey] = nil
|
||||
keyCache[key] = nil
|
||||
currentOutput[prevKeyOut] = nil
|
||||
keyCache[keyIn] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in currentData do
|
||||
if value == previousData[key] then
|
||||
for keyIn, valueIn in currentInput do
|
||||
if valueIn == previousInput[keyIn] then
|
||||
continue
|
||||
end
|
||||
|
||||
local valueOut, keyOut = map(value, key)
|
||||
local valueOut, keyOut = map(valueIn, keyIn)
|
||||
|
||||
if currentOutput[keyOut] ~= valueOut then
|
||||
if currentOutput == previousOutput then
|
||||
currentOutput = table.clone(currentOutput)
|
||||
end
|
||||
currentOutput[keyOut] = valueOut
|
||||
keyCache[key] = keyOut
|
||||
keyCache[keyIn] = keyOut
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -64,12 +64,12 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
|
|||
end)
|
||||
end
|
||||
|
||||
--- Creates a new atom that is computed by mapping over the entries of an
|
||||
--- existing atom. The returned atom is read-only, and its value is
|
||||
--- automatically updated when the source atom changes.
|
||||
--- Creates a new read-only signal that is computed by mapping over the entries
|
||||
--- of an existing atom. If the key is omitted from the mapper's return value,
|
||||
--- the original key is preserved.
|
||||
---
|
||||
--- @param getter A function that returns the table to map over.
|
||||
--- @param mapper A function that transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
|
||||
--- @param getter Returns the table to map over.
|
||||
--- @param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
||||
local mapped = mapped :: Mapped
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,21 @@ local effect = core.effect
|
|||
local effectScope = core.effectScope
|
||||
local onCleanup = core.onCleanup
|
||||
|
||||
--- Calls the observer function for each unique key in the table returned by the
|
||||
--- getter. The observer is called once for each key, and again when a new key
|
||||
--- Calls the callback function for each unique key in the table returned by the
|
||||
--- getter. The callback is called once for each key, and again when a new key
|
||||
--- is added.
|
||||
---
|
||||
--- The observer may return a cleanup function, which will be called when the
|
||||
--- key is removed or when the entire observer is disposed.
|
||||
--- The callback may return a cleanup function, which will be called when the
|
||||
--- key is removed or when the entire callback is disposed.
|
||||
---
|
||||
--- Effects created within the callback are cleaned up when the item is removed
|
||||
--- from the table or when the callback is disposed.
|
||||
---
|
||||
--- @param getter A function that returns the table to observe.
|
||||
--- @param observer A function that is called for each key in the table.
|
||||
--- @param callback A function that is called for each key in the table.
|
||||
--- @return A function for disposing the observer.
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-observer
|
||||
local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||
--- @see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
||||
local function observe<K, V>(getter: () -> { [K]: V }, callback: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||
local scopes: { [K]: () -> () } = {}
|
||||
local currentItems: { [K]: V } = {}
|
||||
|
||||
|
|
@ -33,7 +36,7 @@ local function observe<K, V>(getter: () -> { [K]: V }, observer: (item: V, key:
|
|||
for key, value in currentItems do
|
||||
if previousItems[key] == nil and scopes[key] == nil then
|
||||
scopes[key] = effectScope(function()
|
||||
return observer(value, key)
|
||||
return callback(value, key)
|
||||
end, true)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("useAtom", function()
|
||||
require("./useAtom.spec")
|
||||
suite.describe("useSignalState", function()
|
||||
require("./useSignalState.spec")
|
||||
end)
|
||||
|
||||
suite.describe("useAtomBinding", function()
|
||||
require("./useAtomBinding.spec")
|
||||
suite.describe("useSignalBinding", function()
|
||||
require("./useSignalBinding.spec")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -3,45 +3,43 @@ local ReactCharm = require("../")
|
|||
local renderHook = require("./helpers/renderHook")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local useAtomBinding = ReactCharm.useAtomBinding
|
||||
local test = suite.test
|
||||
|
||||
test("tracks atom state", function()
|
||||
local state = atom(0)
|
||||
test("tracks signal state", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtomBinding(state)
|
||||
return ReactCharm.useSignalBinding(getState)
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
end)
|
||||
|
||||
test("accepts a selector", function()
|
||||
local state = atom(0)
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtomBinding(function()
|
||||
return 2 * state()
|
||||
return ReactCharm.useSignalBinding(function()
|
||||
return 2 * getState()
|
||||
end)
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 2, "should return 2")
|
||||
end)
|
||||
|
||||
test("tracks dependencies", function()
|
||||
local state = atom(0)
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtomBinding(state, { state })
|
||||
return ReactCharm.useSignalBinding(getState, { getState })
|
||||
end)
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
|
||||
state = atom(0)
|
||||
getState, setState = Charm.signal(0)
|
||||
hook.rerender()
|
||||
assert(hook.result:getValue() == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result:getValue() == 1, "should return 1")
|
||||
end)
|
||||
|
||||
|
|
@ -3,45 +3,43 @@ local ReactCharm = require("../")
|
|||
local renderHook = require("./helpers/renderHook")
|
||||
local suite = require("../../../../test/suite")
|
||||
|
||||
local atom = Charm.atom
|
||||
local useAtom = ReactCharm.useAtom
|
||||
local test = suite.test
|
||||
|
||||
test("tracks atom state", function()
|
||||
local state = atom(0)
|
||||
test("tracks signal state", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(state)
|
||||
return ReactCharm.useSignalState(getState)
|
||||
end)
|
||||
assert(hook.result == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result == 1, "should return 1")
|
||||
end)
|
||||
|
||||
test("accepts a selector", function()
|
||||
local state = atom(0)
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(function()
|
||||
return 2 * state()
|
||||
return ReactCharm.useSignalState(function()
|
||||
return 2 * getState()
|
||||
end)
|
||||
end)
|
||||
assert(hook.result == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result == 2, "should return 2")
|
||||
end)
|
||||
|
||||
test("tracks dependencies", function()
|
||||
local state = atom(0)
|
||||
local getState, setState = Charm.signal(0)
|
||||
local hook = renderHook(function()
|
||||
return useAtom(state, { state })
|
||||
return ReactCharm.useSignalState(getState, { getState })
|
||||
end)
|
||||
assert(hook.result == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result == 1, "should return 1")
|
||||
|
||||
state = atom(0)
|
||||
getState, setState = Charm.signal(0)
|
||||
hook.rerender()
|
||||
assert(hook.result == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(hook.result == 1, "should return 1")
|
||||
end)
|
||||
|
||||
30
packages/react-charm/src/index.d.ts
vendored
30
packages/react-charm/src/index.d.ts
vendored
|
|
@ -1,29 +1,31 @@
|
|||
import { Binding } from "@rbxts/react";
|
||||
|
||||
/**
|
||||
* A hook that subscribes to changes in the given atom or selector. The
|
||||
* component is re-rendered with the new state after an update.
|
||||
* Returns a React state that updates when the getter function updates.
|
||||
* Re-renders the component when the value changes.
|
||||
*
|
||||
* If the `dependencies` array is provided, a new effect is created for the
|
||||
* current `callback` when the dependencies change. Otherwise, the effect is
|
||||
* current `getter` when the dependencies change. Otherwise, the effect is
|
||||
* created only once, when the component is mounted.
|
||||
*
|
||||
* @param callback The atom or selector to watch.
|
||||
* @param dependencies An array of values used to memoize the callback.
|
||||
* @returns The current state.
|
||||
* @param getter The signal or selector to watch.
|
||||
* @param dependencies An array of values used to memoize the getter.
|
||||
* @return The current state.
|
||||
*/
|
||||
export function useAtom<T>(callback: () => T, dependencies?: any[]): T;
|
||||
export function useSignalState<T>(getter: () => T, dependencies?: any[]): T;
|
||||
|
||||
/**
|
||||
* A hook that subscribes to changes in the given atom or selector. Returns
|
||||
* a binding that updates when the state changes.
|
||||
* Returns a React binding that updates when the getter function updates.
|
||||
*
|
||||
* If the `dependencies` array is provided, a new effect is created for the
|
||||
* current `callback` when the dependencies change. Otherwise, the effect is
|
||||
* current `getter` when the dependencies change. Otherwise, the effect is
|
||||
* created only once, when the component is mounted.
|
||||
*
|
||||
* @param callback The atom or selector to watch.
|
||||
* @param dependencies An array of values used to memoize the callback.
|
||||
* @returns A binding that stores the current state.
|
||||
* @param getter The atom or selector to watch.
|
||||
* @param dependencies An array of values used to memoize the getter.
|
||||
* @return A binding that stores the current state.
|
||||
*/
|
||||
export function useAtomBinding<T>(callback: () => T, dependencies?: any[]): Binding<T>;
|
||||
export function useSignalBinding<T>(getter: () => T, dependencies?: any[]): Binding<T>;
|
||||
|
||||
// Aliases
|
||||
export { useSignalState as useAtom };
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
local Charm = require("./Charm")
|
||||
local React = require("./React")
|
||||
|
||||
--- A hook that subscribes to changes in the given atom or selector. The
|
||||
--- component is re-rendered with the new state after an update.
|
||||
--- Returns a React state that updates when the getter function updates.
|
||||
--- Re-renders the component when the value changes.
|
||||
---
|
||||
--- If the `dependencies` array is provided, a new effect is created for the
|
||||
--- current `getter` when the dependencies change. Otherwise, the effect is
|
||||
--- created only once, when the component is mounted.
|
||||
---
|
||||
--- @param getter The atom or selector to watch.
|
||||
--- @param getter The signal or selector to watch.
|
||||
--- @param dependencies An array of values used to memoize the getter.
|
||||
--- @return The current state.
|
||||
local function useAtom<T>(getter: () -> T, dependencies: { any }?): T
|
||||
local state, setState = React.useState(getter)
|
||||
local function useSignalState<T>(getter: () -> T, dependencies: { any }?): T
|
||||
local value, setValue = React.useState(getter)
|
||||
|
||||
React.useLayoutEffect(function()
|
||||
return Charm.effect(function()
|
||||
-- Wrap in untracked to avoid tracking inner effect and atom calls
|
||||
Charm.untracked(setState, getter())
|
||||
Charm.untracked(setValue, getter())
|
||||
end)
|
||||
end, dependencies or {})
|
||||
|
||||
return state
|
||||
return value
|
||||
end
|
||||
|
||||
--- A hook that subscribes to changes in the given atom or selector. Returns
|
||||
--- a binding that updates when the state changes.
|
||||
--- Returns a React binding that updates when the getter function updates.
|
||||
---
|
||||
--- If the `dependencies` array is provided, a new effect is created for the
|
||||
--- current `getter` when the dependencies change. Otherwise, the effect is
|
||||
|
|
@ -34,9 +33,9 @@ end
|
|||
--- @param getter The atom or selector to watch.
|
||||
--- @param dependencies An array of values used to memoize the getter.
|
||||
--- @return A binding that stores the current state.
|
||||
local function useAtomBinding<T>(getter: () -> T, dependencies: { any }?)
|
||||
local initialState = React.useMemo(getter, {})
|
||||
local binding, setBinding = React.useBinding(initialState)
|
||||
local function useSignalBinding<T>(getter: () -> T, dependencies: { any }?)
|
||||
local initialValue = React.useMemo(getter, {})
|
||||
local binding, setBinding = React.useBinding(initialValue)
|
||||
|
||||
React.useLayoutEffect(function()
|
||||
return Charm.effect(function()
|
||||
|
|
@ -49,6 +48,7 @@ local function useAtomBinding<T>(getter: () -> T, dependencies: { any }?)
|
|||
end
|
||||
|
||||
return {
|
||||
useAtom = useAtom,
|
||||
useAtomBinding = useAtomBinding,
|
||||
useSignalState = useSignalState,
|
||||
useSignalBinding = useSignalBinding,
|
||||
useAtom = useSignalState,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local suite = require("../../../../test/suite")
|
||||
|
||||
suite.describe("useAtom", function()
|
||||
require("./useAtom.spec")
|
||||
suite.describe("useSignalState", function()
|
||||
require("./useSignalState.spec")
|
||||
end)
|
||||
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -5,55 +5,55 @@ local suite = require("../../../../test/suite")
|
|||
|
||||
local test = suite.test
|
||||
|
||||
test("tracks atom state", function()
|
||||
local state = Charm.atom(0)
|
||||
test("tracks signal state", function()
|
||||
local getState, setState = Charm.signal(0)
|
||||
local source
|
||||
local cleanup = Vide.root(function()
|
||||
source = VideCharm.useAtom(state)
|
||||
source = VideCharm.useSignalState(getState)
|
||||
end)
|
||||
assert(source() == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(source() == 1, "should return 1")
|
||||
cleanup()
|
||||
end)
|
||||
|
||||
test("accepts a selector", function()
|
||||
local state = Charm.atom(0)
|
||||
local getState, setState = Charm.signal(0)
|
||||
local source
|
||||
local cleanup = Vide.root(function()
|
||||
source = VideCharm.useAtom(function()
|
||||
return 2 * state()
|
||||
source = VideCharm.useSignalState(function()
|
||||
return 2 * getState()
|
||||
end)
|
||||
end)
|
||||
assert(source() == 0, "should return 0")
|
||||
state(1)
|
||||
setState(1)
|
||||
assert(source() == 2, "should return 2")
|
||||
cleanup()
|
||||
end)
|
||||
|
||||
test("updates source in untracked", function()
|
||||
local outerAtom = Charm.atom(nil :: any)
|
||||
local innerAtom = Charm.atom(0)
|
||||
local getOuter, setOuter = Charm.signal(nil :: any)
|
||||
local getInner, setInner = Charm.signal(0)
|
||||
local spyCalls = 0
|
||||
local cleanup = Vide.root(function()
|
||||
return Vide.show(VideCharm.useAtom(outerAtom), function()
|
||||
-- Charm effects created in Vide scopes triggered by useAtom might
|
||||
-- incorrectly get treated as an inner effect in useAtom. This test
|
||||
return Vide.show(VideCharm.useSignalState(getOuter), function()
|
||||
-- Charm effects created in Vide scopes triggered by useSignalState might
|
||||
-- incorrectly get treated as an inner effect in useSignalState. This test
|
||||
-- ensures that doesn't happen.
|
||||
Vide.cleanup(Charm.effect(function()
|
||||
innerAtom()
|
||||
getInner()
|
||||
spyCalls += 1
|
||||
end))
|
||||
return nil
|
||||
end)
|
||||
end)
|
||||
assert(spyCalls == 0, "should not call spy initially")
|
||||
outerAtom(true)
|
||||
setOuter(true)
|
||||
assert(spyCalls == 1, "should call spy when visible")
|
||||
innerAtom(1)
|
||||
setInner(1)
|
||||
assert(spyCalls == 2, "spy should run when counter updates")
|
||||
outerAtom({}) -- set to a truthy value to trigger an outer effect re-run
|
||||
innerAtom(2)
|
||||
setOuter({}) -- set to a truthy value to trigger an outer effect re-run
|
||||
setInner(2)
|
||||
assert(spyCalls == 3, "spy should still run when counter updates")
|
||||
cleanup()
|
||||
end)
|
||||
13
packages/vide-charm/src/index.d.ts
vendored
13
packages/vide-charm/src/index.d.ts
vendored
|
|
@ -1,8 +1,11 @@
|
|||
/**
|
||||
* Subscribes to the state of an atom or selector. Returns a source that
|
||||
* updates when the state changes.
|
||||
* Returns a reactive source that subscribes to the current value of the given
|
||||
* signal or getter function.
|
||||
*
|
||||
* @param callback The atom or selector to subscribe to.
|
||||
* @returns The reactive source.
|
||||
* @param getter The getter function to subscribe to.
|
||||
* @return The reactive source.
|
||||
*/
|
||||
export function useAtom<T>(callback: () => T, dependencies?: any[]): () => T;
|
||||
export function useSignalState<T>(getter: () => T, dependencies?: any[]): () => T;
|
||||
|
||||
// Aliases
|
||||
export { useSignalState as useAtom };
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
local Charm = require("./Charm")
|
||||
local Vide = require("./Vide")
|
||||
|
||||
--- Returns a reactive source that updates when the given getter function's
|
||||
--- value changes.
|
||||
--- Returns a reactive source that subscribes to the current value of the given
|
||||
--- signal or getter function.
|
||||
---
|
||||
--- @param getter The atom or selector to subscribe to.
|
||||
--- @param getter The getter function to subscribe to.
|
||||
--- @return The reactive source.
|
||||
local function useAtom<State>(getter: () -> State): () -> State
|
||||
local function useSignalState<State>(getter: () -> State): () -> State
|
||||
local state = Vide.source(getter())
|
||||
|
||||
Vide.cleanup(Charm.effect(function()
|
||||
|
|
@ -18,5 +18,6 @@ local function useAtom<State>(getter: () -> State): () -> State
|
|||
end
|
||||
|
||||
return {
|
||||
useAtom = useAtom,
|
||||
useSignalState = useSignalState,
|
||||
useAtom = useSignalState,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue