mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
refactor: improve source readability
This commit is contained in:
parent
e08204a9d1
commit
a44355b3b6
4 changed files with 389 additions and 193 deletions
|
|
@ -1,7 +1,47 @@
|
||||||
local fs = require("@std/fs")
|
local fs = require("@std/fs")
|
||||||
|
local json = require("@std/json")
|
||||||
local net = require("@lute/net")
|
local net = require("@lute/net")
|
||||||
local process = require("@lute/process")
|
local process = require("@lute/process")
|
||||||
|
|
||||||
|
local CURRENT_FFLAGS = "https://clientsettingscdn.roblox.com/v1/settings/application?applicationName=PCStudioApp"
|
||||||
|
local FFLAG_KINDS = { "FFlag", "FInt" }
|
||||||
|
|
||||||
|
local function analyze()
|
||||||
|
local args = {
|
||||||
|
"luau-lsp",
|
||||||
|
"analyze",
|
||||||
|
"--sourcemap=sourcemap.json",
|
||||||
|
"--ignore=**/node_modules/**",
|
||||||
|
"--base-luaurc=.luaurc",
|
||||||
|
"--definitions=roblox.d.luau",
|
||||||
|
"--no-flags-enabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
local response = net.request(CURRENT_FFLAGS, { method = "GET" })
|
||||||
|
local data: any = json.deserialize(response.body)
|
||||||
|
|
||||||
|
for name, value in data.applicationSettings do
|
||||||
|
for _, kind in FFLAG_KINDS do
|
||||||
|
if string.find(name, `^{kind}Luau`) then
|
||||||
|
table.insert(args, `--flag:{string.sub(name, #kind + 1)}={value}`)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(args, "packages")
|
||||||
|
table.insert(args, "tests")
|
||||||
|
table.insert(args, "benches")
|
||||||
|
|
||||||
|
local result = process.run(args)
|
||||||
|
local stderr = string.gsub(result.stderr, "Unknown FFlag[^\n]+\n", "")
|
||||||
|
|
||||||
|
print(stderr)
|
||||||
|
|
||||||
|
if result.exitcode == 1 then
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
process.run({ "rojo", "sourcemap", "--output=sourcemap.json" }, { stdio = "inherit" })
|
process.run({ "rojo", "sourcemap", "--output=sourcemap.json" }, { stdio = "inherit" })
|
||||||
|
|
||||||
fs.writestringtofile(
|
fs.writestringtofile(
|
||||||
|
|
@ -9,17 +49,19 @@ fs.writestringtofile(
|
||||||
net.request("https://luau-lsp.pages.dev/globalTypes.None.d.luau", { method = "GET" }).body
|
net.request("https://luau-lsp.pages.dev/globalTypes.None.d.luau", { method = "GET" }).body
|
||||||
)
|
)
|
||||||
|
|
||||||
process.run({
|
analyze()
|
||||||
"luau-lsp",
|
|
||||||
"analyze",
|
-- process.run({
|
||||||
"--sourcemap=sourcemap.json",
|
-- "luau-lsp",
|
||||||
"--ignore=**/node_modules/**",
|
-- "analyze",
|
||||||
"--base-luaurc=.luaurc",
|
-- "--sourcemap=sourcemap.json",
|
||||||
"--defs=roblox.d.luau",
|
-- "--ignore=**/node_modules/**",
|
||||||
"packages",
|
-- "--base-luaurc=.luaurc",
|
||||||
"tests",
|
-- "--defs=roblox.d.luau",
|
||||||
"benches",
|
-- "packages",
|
||||||
}, { stdio = "inherit" })
|
-- "tests",
|
||||||
|
-- "benches",
|
||||||
|
-- }, { stdio = "inherit" })
|
||||||
|
|
||||||
fs.remove("roblox.d.luau")
|
fs.remove("roblox.d.luau")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
|
--[[
|
||||||
|
Reactive system adapted from alien-signals at
|
||||||
|
https://github.com/stackblitz/alien-signals/blob/master/src/system.ts
|
||||||
|
]]
|
||||||
|
|
||||||
local system = require("@self/system")
|
local system = require("@self/system")
|
||||||
|
|
||||||
export type ReactiveFlags = system.ReactiveFlags
|
|
||||||
|
|
||||||
export type ReactiveNode = system.ReactiveNode
|
|
||||||
|
|
||||||
export type Link = system.Link
|
export type Link = system.Link
|
||||||
|
export type ReactiveFlags = system.ReactiveFlags
|
||||||
|
export type ReactiveNode = system.ReactiveNode
|
||||||
|
|
||||||
export type Atom<T = any> = (...((T) -> T) | T) -> T
|
export type Atom<T = any> = (...((T) -> T) | T) -> T
|
||||||
|
|
||||||
|
|
@ -36,30 +39,34 @@ type SignalNode<T = any> = ReactiveNode & {
|
||||||
pendingValue: T,
|
pendingValue: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
local NONE = 0b0000000
|
-- Bitmask flags for tracking the state of reactive nodes. Flags are combined
|
||||||
local MUTABLE = 0b0000001
|
-- and checked using bitwise operations for efficiency.
|
||||||
local WATCHING = 0b0000010
|
local NONE = 0b0000000 -- Default state
|
||||||
local RECURSED_CHECK = 0b0000100
|
local MUTABLE = 0b0000001 -- Can be changed (signal or computed)
|
||||||
local DIRTY = 0b0010000
|
local WATCHING = 0b0000010 -- Watching for changes (effects)
|
||||||
local PENDING = 0b0100000
|
local RECURSED_CHECK = 0b0000100 -- Being checked for circular dependencies
|
||||||
local PEEKING = 0b1000000
|
local DIRTY = 0b0010000 -- Value has changed and needs updating
|
||||||
|
local PENDING = 0b0100000 -- Might be dirty, pending verification
|
||||||
|
local PEEKING = 0b1000000 -- Blocking dependency tracking
|
||||||
|
|
||||||
local function inlineTest(): string
|
local function inlineTest(): string
|
||||||
return debug.info(1, "n")
|
return debug.info(1, "n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local IS_O2 = inlineTest() ~= "inlineTest"
|
||||||
|
|
||||||
local flags = {
|
local flags = {
|
||||||
--[[
|
--[[
|
||||||
Enforces synchronous, non-yielding behavior in signals and effects.
|
Enforces synchronous, non-yielding behavior in signals and effects.
|
||||||
Also enables state validation in Charm Sync to catch sync errors early.
|
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.
|
Enabled if the optimization level is below 2, which is true in studio.
|
||||||
]]
|
]]
|
||||||
strict = inlineTest() == "inlineTest",
|
strict = not IS_O2,
|
||||||
--[[
|
--[[
|
||||||
Enforces immutability of tables 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.
|
if the optimization level is below 2, which is true in studio.
|
||||||
]]
|
]]
|
||||||
frozen = inlineTest() == "inlineTest",
|
frozen = not IS_O2,
|
||||||
--[[
|
--[[
|
||||||
Whether parent effects should track inner effects and clean them up
|
Whether parent effects should track inner effects and clean them up
|
||||||
when the parent effect re-runs. This is usually desirable, but can be
|
when the parent effect re-runs. This is usually desirable, but can be
|
||||||
|
|
@ -68,27 +75,45 @@ local flags = {
|
||||||
trackInnerEffects = true,
|
trackInnerEffects = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local ReactiveProxy = {
|
||||||
|
proxyMap = setmetatable({} :: { [any]: ReactiveProxy }, { __mode = "v" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Alien Signals reactive system
|
||||||
local link: (dep: ReactiveNode, sub: ReactiveNode, version: number) -> ()
|
local link: (dep: ReactiveNode, sub: ReactiveNode, version: number) -> ()
|
||||||
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
|
||||||
local propagate: (link: Link) -> ()
|
local propagate: (link: Link) -> ()
|
||||||
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
|
||||||
local shallowPropagate: (link: Link) -> ()
|
local shallowPropagate: (link: Link) -> ()
|
||||||
|
|
||||||
local ReactiveProxy = {
|
-- Global version counter for dependency link deduplication. Incremented at the
|
||||||
proxyMap = setmetatable({} :: { [any]: ReactiveProxy }, { __mode = "v" }),
|
-- start of each effect/computed run to ensure links created in the same cycle
|
||||||
}
|
-- are deduplicated.
|
||||||
|
|
||||||
local cycle = 0
|
local cycle = 0
|
||||||
local batchDepth = 0
|
-- Effects runs are queued to prevent redundant executions when multiple
|
||||||
local notifyIndex = 0
|
-- signals update in the same batch.
|
||||||
local queued: { EffectNode? } = {}
|
local queued: { EffectNode? } = {}
|
||||||
local queuedLength = 0
|
local queuedLength = 0
|
||||||
|
local notifyIndex = 0
|
||||||
|
-- Batch operations group multiple updates together, deferring effect runs
|
||||||
|
-- until the batch completes. Effects only run when the outermost batch ends.
|
||||||
|
local batchDepth = 0
|
||||||
|
-- Tracks the currently active effect or computed signal during execution.
|
||||||
|
-- Dependencies accessed during this time will link to the active subscriber.
|
||||||
local activeSub: ReactiveNode?
|
local activeSub: ReactiveNode?
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Wraps a function to prevent yielding when in strict mode. If the function
|
||||||
|
attempts to yield, an error is thrown.
|
||||||
|
|
||||||
|
@param fn The function to wrap.
|
||||||
|
@return A wrapped function that cannot yield in strict mode.
|
||||||
|
]]
|
||||||
local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args...) -> Results...
|
local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args...) -> Results...
|
||||||
if not flags.strict then
|
if not flags.strict then
|
||||||
return fn
|
return fn
|
||||||
end
|
end
|
||||||
|
|
||||||
local function resultHandler(thread: thread, ok: boolean, ...)
|
local function resultHandler(thread: thread, ok: boolean, ...)
|
||||||
if not ok then
|
if not ok then
|
||||||
error(debug.traceback(thread, tostring((...))))
|
error(debug.traceback(thread, tostring((...))))
|
||||||
|
|
@ -97,31 +122,57 @@ local function noYield<Args..., Results...>(fn: (Args...) -> Results...): (Args.
|
||||||
end
|
end
|
||||||
return ...
|
return ...
|
||||||
end
|
end
|
||||||
|
|
||||||
return function(...)
|
return function(...)
|
||||||
local thread = coroutine.create(fn)
|
local thread = coroutine.create(fn)
|
||||||
return resultHandler(thread, coroutine.resume(thread, ...))
|
return resultHandler(thread, coroutine.resume(thread, ...))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function deepFreeze(object: any)
|
--[[
|
||||||
if type(object) == "table" and not table.isfrozen(object) and getmetatable(object) == nil then
|
Recursively freezes a table and all nested tables to enforce immutability.
|
||||||
table.freeze(object)
|
Tables with metatables are skipped, as they are assumed to be mutable.
|
||||||
for _, value in object do
|
|
||||||
deepFreeze(value)
|
@param value Table to freeze
|
||||||
|
]]
|
||||||
|
local function deepFreeze(value: any)
|
||||||
|
if type(value) == "table" and not table.isfrozen(value) and getmetatable(value) == nil then
|
||||||
|
table.freeze(value)
|
||||||
|
for _, entry in value do
|
||||||
|
deepFreeze(entry)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns the currently active subscriber (effect or computed node).
|
||||||
|
|
||||||
|
@return Current active subscriber or `nil`
|
||||||
|
]]
|
||||||
local function getActiveSub(): ReactiveNode?
|
local function getActiveSub(): ReactiveNode?
|
||||||
return activeSub
|
return activeSub
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Sets the currently active subscriber (effect or computed node) and returns
|
||||||
|
the previous subscriber.
|
||||||
|
|
||||||
|
@param sub New subscriber to set as active
|
||||||
|
@return Previous active subscriber
|
||||||
|
]]
|
||||||
local function setActiveSub(sub: ReactiveNode?)
|
local function setActiveSub(sub: ReactiveNode?)
|
||||||
local prevSub = activeSub
|
local prevSub = activeSub
|
||||||
activeSub = sub
|
activeSub = sub
|
||||||
return prevSub
|
return prevSub
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Removes dependencies from the subscriber's dependency list. When called
|
||||||
|
after tracking is complete, this ensures that only currently tracked
|
||||||
|
dependencies remain linked.
|
||||||
|
|
||||||
|
@param sub Effect or computed node
|
||||||
|
]]
|
||||||
local function purgeDeps(sub: ReactiveNode)
|
local function purgeDeps(sub: ReactiveNode)
|
||||||
local depsTail = sub.depsTail
|
local depsTail = sub.depsTail
|
||||||
local dep = if depsTail then depsTail.nextDep else sub.deps
|
local dep = if depsTail then depsTail.nextDep else sub.deps
|
||||||
|
|
@ -130,41 +181,58 @@ local function purgeDeps(sub: ReactiveNode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function runCleanups(effect: EffectScopeNode)
|
--[[
|
||||||
local cleanups = effect.cleanups
|
Runs all cleanup functions registered with the effect node. Any errors
|
||||||
|
thrown during cleanup are caught and logged as warnings.
|
||||||
|
|
||||||
|
@param cleanups List of cleanup functions
|
||||||
|
]]
|
||||||
|
local function runCleanups(cleanups: { () -> () })
|
||||||
for key, cleanup in cleanups do
|
for key, cleanup in cleanups do
|
||||||
local prevSub = setActiveSub(nil)
|
|
||||||
local success, result: any = pcall(cleanup)
|
|
||||||
activeSub = prevSub
|
|
||||||
cleanups[key] = nil
|
cleanups[key] = nil
|
||||||
if not success then
|
local prevSub = setActiveSub(nil)
|
||||||
warn("An error occurred during effect cleanup:", result)
|
xpcall(cleanup, warn or print)
|
||||||
end
|
activeSub = prevSub
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function run(e: EffectNode)
|
--[[
|
||||||
local flags = e.flags
|
Checks if the effect node needs to run based on its flags and dependencies.
|
||||||
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(e.deps :: Link, e)) then
|
Effects run if they are marked dirty or pending with dirty dependencies.
|
||||||
runCleanups(e)
|
|
||||||
|
@param effect Effect node to run
|
||||||
|
]]
|
||||||
|
local function run(effect: EffectNode)
|
||||||
|
local flags = effect.flags
|
||||||
|
|
||||||
|
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
|
||||||
|
runCleanups(effect.cleanups)
|
||||||
|
|
||||||
cycle += 1
|
cycle += 1
|
||||||
e.depsTail = nil
|
effect.depsTail = nil
|
||||||
e.flags = bit32.bor(WATCHING, RECURSED_CHECK)
|
effect.flags = bit32.bor(WATCHING, RECURSED_CHECK)
|
||||||
local prevSub = setActiveSub(e)
|
|
||||||
local success, result = pcall(e.fn)
|
local prevSub = setActiveSub(effect)
|
||||||
|
local success, result = pcall(effect.fn)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
|
|
||||||
purgeDeps(e)
|
effect.flags = bit32.band(effect.flags, bit32.bnot(RECURSED_CHECK))
|
||||||
|
purgeDeps(effect)
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(e.cleanups, noYield(result))
|
table.insert(effect.cleanups, noYield(result))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
e.flags = WATCHING
|
effect.flags = WATCHING
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Processes the queued effect runs. Effects are run in the order they were
|
||||||
|
queued, ensuring that all updates are processed.
|
||||||
|
]]
|
||||||
local function flush()
|
local function flush()
|
||||||
while notifyIndex < queuedLength do
|
while notifyIndex < queuedLength do
|
||||||
local effect = queued[notifyIndex] :: EffectNode
|
local effect = queued[notifyIndex] :: EffectNode
|
||||||
|
|
@ -172,14 +240,23 @@ local function flush()
|
||||||
notifyIndex += 1
|
notifyIndex += 1
|
||||||
run(effect)
|
run(effect)
|
||||||
end
|
end
|
||||||
|
|
||||||
notifyIndex = 0
|
notifyIndex = 0
|
||||||
queuedLength = 0
|
queuedLength = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Starts a batch operation, incrementing the batch depth counter. Effects
|
||||||
|
will not run until the outermost batch ends.
|
||||||
|
]]
|
||||||
local function startBatch()
|
local function startBatch()
|
||||||
batchDepth += 1
|
batchDepth += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Ends a batch operation, decrementing the batch depth counter. If this is
|
||||||
|
the outermost batch, queued effects are flushed and run.
|
||||||
|
]]
|
||||||
local function endBatch()
|
local function endBatch()
|
||||||
batchDepth -= 1
|
batchDepth -= 1
|
||||||
if batchDepth == 0 then
|
if batchDepth == 0 then
|
||||||
|
|
@ -187,56 +264,88 @@ local function endBatch()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function updateComputed(c: ComputedNode): boolean
|
--[[
|
||||||
|
Updates a computed signal by re-evaluating its getter function. Returns
|
||||||
|
whether the computed value changed.
|
||||||
|
|
||||||
|
@param computed Computed node to update
|
||||||
|
@return Boolean indicating whether the computed value changed
|
||||||
|
]]
|
||||||
|
local function updateComputed(computed: ComputedNode): boolean
|
||||||
cycle += 1
|
cycle += 1
|
||||||
c.depsTail = nil
|
computed.depsTail = nil
|
||||||
c.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
computed.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||||
local prevSub = setActiveSub(c)
|
|
||||||
local oldValue = c.value
|
local prevSub = setActiveSub(computed)
|
||||||
local success, result = pcall(c.getter, oldValue)
|
local oldValue = computed.value
|
||||||
|
local success, newValue = pcall(computed.getter, oldValue)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
c.flags = bit32.band(c.flags, bit32.bnot(RECURSED_CHECK))
|
|
||||||
purgeDeps(c)
|
computed.flags = bit32.band(computed.flags, bit32.bnot(RECURSED_CHECK))
|
||||||
if not success then
|
purgeDeps(computed)
|
||||||
error(result)
|
|
||||||
|
if success then
|
||||||
|
computed.value = newValue
|
||||||
|
return oldValue ~= newValue
|
||||||
|
else
|
||||||
|
error(newValue)
|
||||||
end
|
end
|
||||||
if oldValue ~= result then
|
end
|
||||||
c.value = result
|
|
||||||
|
--[[
|
||||||
|
Updates a signal by applying its pending value. Returns whether the
|
||||||
|
signal's value changed.
|
||||||
|
|
||||||
|
@param signal Signal node to update
|
||||||
|
@return Boolean indicating whether the signal's value changed
|
||||||
|
]]
|
||||||
|
local function updateSignal(signal: SignalNode): boolean
|
||||||
|
signal.flags = MUTABLE
|
||||||
|
local pendingValue = signal.pendingValue
|
||||||
|
if signal.currentValue ~= pendingValue then
|
||||||
|
signal.currentValue = pendingValue
|
||||||
return true
|
return true
|
||||||
else
|
else
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function updateSignal(s: SignalNode): boolean
|
--[[
|
||||||
s.flags = MUTABLE
|
Clears the pending flag from a subscriber. Used inside of if conditions to
|
||||||
local pendingValue = s.pendingValue
|
inline the operation.
|
||||||
if s.currentValue ~= pendingValue then
|
|
||||||
s.currentValue = pendingValue
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
|
@param sub Effect or computed node
|
||||||
|
@param flags Subscriber's current flags
|
||||||
|
]]
|
||||||
|
local function clearPending(sub: ReactiveNode, flags: ReactiveFlags)
|
||||||
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
|
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns a function that evaluates the computed value when accessed.
|
||||||
|
Computed values are lazily evaluated and cached unless marked dirty or
|
||||||
|
pending with dirty dependencies.
|
||||||
|
|
||||||
|
@param this Computed node
|
||||||
|
@return Operator to read the computed value
|
||||||
|
]]
|
||||||
local function computedOper<T>(this: ComputedNode<T>): () -> T
|
local function computedOper<T>(this: ComputedNode<T>): () -> T
|
||||||
local function readComputed(): T
|
local function readComputed(): T
|
||||||
local flags = this.flags
|
local flags = this.flags
|
||||||
|
|
||||||
if
|
if
|
||||||
bit32.btest(flags, DIRTY)
|
bit32.btest(flags, DIRTY)
|
||||||
or (bit32.btest(flags, PENDING) and (checkDirty(this.deps :: Link, this) or setNotPending(this, flags)))
|
or (bit32.btest(flags, PENDING) and (checkDirty(this.deps :: Link, this) or clearPending(this, flags)))
|
||||||
then
|
then
|
||||||
if updateComputed(this) then
|
if updateComputed(this) then
|
||||||
|
-- Notify subscribers if the value changed
|
||||||
local subs = this.subs
|
local subs = this.subs
|
||||||
if subs then
|
if subs then
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elseif flags == NONE then
|
elseif flags == NONE then
|
||||||
|
-- First access should initialize the computed value
|
||||||
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
this.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
|
||||||
local prevSub = setActiveSub(this)
|
local prevSub = setActiveSub(this)
|
||||||
local success, result = pcall(this.getter)
|
local success, result = pcall(this.getter)
|
||||||
|
|
@ -248,31 +357,50 @@ local function computedOper<T>(this: ComputedNode<T>): () -> T
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if activeSub then
|
if activeSub then
|
||||||
link(this, activeSub, cycle)
|
link(this, activeSub, cycle)
|
||||||
end
|
end
|
||||||
|
|
||||||
return this.value :: T
|
return this.value :: T
|
||||||
end
|
end
|
||||||
|
|
||||||
return readComputed
|
return readComputed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns functions to read and write a signal's value:
|
||||||
|
- Setter: Updates the signal's value and notifies subscribers
|
||||||
|
- Getter: Returns the current value and registers dependency
|
||||||
|
|
||||||
|
@param this Signal node
|
||||||
|
@param equals Optional comparator function to check for value changes
|
||||||
|
@return Operator to get the signal value
|
||||||
|
@return Operator to set the signal value
|
||||||
|
]]
|
||||||
local function signalOper<T>(
|
local function signalOper<T>(
|
||||||
this: SignalNode<T>,
|
this: SignalNode<T>,
|
||||||
equals: ((current: T, incoming: T) -> boolean)?
|
equals: ((current: T, incoming: T) -> boolean)?
|
||||||
): (() -> T, (((T) -> T) | T) -> T)
|
): (() -> T, (((T) -> T) | T) -> T)
|
||||||
local function writeSignal(update: ((T) -> T) | T): T
|
local function writeSignal(update: ((T) -> T) | T): T
|
||||||
local value: T
|
local value: T
|
||||||
|
|
||||||
|
-- Determine the new value based on whether the update is a function or
|
||||||
|
-- a direct value
|
||||||
if type(update) == "function" then
|
if type(update) == "function" then
|
||||||
local prevSub = setActiveSub(nil)
|
local prevSub = setActiveSub(nil)
|
||||||
local success, result = pcall(noYield(update), this.pendingValue)
|
local success, result = pcall(noYield(update), this.pendingValue)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
if not success then
|
if success then
|
||||||
|
value = result
|
||||||
|
else
|
||||||
error(result)
|
error(result)
|
||||||
end
|
end
|
||||||
value = result
|
|
||||||
else
|
else
|
||||||
value = update
|
value = update
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Check if the new value is different from the pending value
|
||||||
if if equals then not equals(this.pendingValue, value) else this.pendingValue ~= value then
|
if if equals then not equals(this.pendingValue, value) else this.pendingValue ~= value then
|
||||||
if flags.frozen then
|
if flags.frozen then
|
||||||
deepFreeze(value)
|
deepFreeze(value)
|
||||||
|
|
@ -288,10 +416,12 @@ local function signalOper<T>(
|
||||||
end
|
end
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
return this.currentValue
|
return this.currentValue
|
||||||
end
|
end
|
||||||
|
|
||||||
local function readSignal(): T
|
local function readSignal(): T
|
||||||
|
-- Check if the signal needs to be updated (for signals within effects)
|
||||||
if bit32.btest(this.flags, DIRTY) then
|
if bit32.btest(this.flags, DIRTY) then
|
||||||
if updateSignal(this) then
|
if updateSignal(this) then
|
||||||
local subs = this.subs
|
local subs = this.subs
|
||||||
|
|
@ -300,6 +430,8 @@ local function signalOper<T>(
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Register this signal as a dependency of the active subscriber
|
||||||
local sub = activeSub
|
local sub = activeSub
|
||||||
while sub do
|
while sub do
|
||||||
if bit32.btest(sub.flags, bit32.bor(MUTABLE, WATCHING)) then
|
if bit32.btest(sub.flags, bit32.bor(MUTABLE, WATCHING)) then
|
||||||
|
|
@ -309,12 +441,25 @@ local function signalOper<T>(
|
||||||
local subs = sub.subs
|
local subs = sub.subs
|
||||||
sub = if subs then subs.sub else nil
|
sub = if subs then subs.sub else nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return this.currentValue
|
return this.currentValue
|
||||||
end
|
end
|
||||||
|
|
||||||
return readSignal, writeSignal
|
return readSignal, writeSignal
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Returns an effect cleanup operator that disposes of the effect. This
|
||||||
|
operator performs a thorough cleanup:
|
||||||
|
|
||||||
|
1. Removes all dependency links
|
||||||
|
2. Unlinks from parent effects/scopes
|
||||||
|
3. Clears state flags to mark the effect as inactive
|
||||||
|
4. Runs all registered cleanup functions
|
||||||
|
|
||||||
|
@param this Effect node
|
||||||
|
@return Effect cleanup operator
|
||||||
|
]]
|
||||||
local function effectOper(this: EffectScopeNode): () -> ()
|
local function effectOper(this: EffectScopeNode): () -> ()
|
||||||
return function()
|
return function()
|
||||||
this.depsTail = nil
|
this.depsTail = nil
|
||||||
|
|
@ -324,10 +469,16 @@ local function effectOper(this: EffectScopeNode): () -> ()
|
||||||
if sub then
|
if sub then
|
||||||
unlink(sub, this)
|
unlink(sub, this)
|
||||||
end
|
end
|
||||||
runCleanups(this)
|
runCleanups(this.cleanups)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Updates a signal or computed value and returns whether the value changed.
|
||||||
|
|
||||||
|
@param node Signal or computed node
|
||||||
|
@return Boolean indicating whether the value changed
|
||||||
|
]]
|
||||||
local function update(node: ReactiveNode): boolean
|
local function update(node: ReactiveNode): boolean
|
||||||
if node.depsTail then
|
if node.depsTail then
|
||||||
return updateComputed(node :: ComputedNode)
|
return updateComputed(node :: ComputedNode)
|
||||||
|
|
@ -336,10 +487,17 @@ local function update(node: ReactiveNode): boolean
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Queues an effect for execution, ensuring inner effects are notified in the
|
||||||
|
correct order.
|
||||||
|
|
||||||
|
@param effect Effect node to notify
|
||||||
|
]]
|
||||||
local function notify(effect: ReactiveNode)
|
local function notify(effect: ReactiveNode)
|
||||||
local insertIndex = queuedLength
|
local insertIndex = queuedLength
|
||||||
local firstInsertedIndex = insertIndex
|
local firstInsertedIndex = insertIndex
|
||||||
|
|
||||||
|
-- Collect inner effects in a chain
|
||||||
repeat
|
repeat
|
||||||
effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING))
|
effect.flags = bit32.band(effect.flags, bit32.bnot(WATCHING))
|
||||||
queued[insertIndex] = effect :: EffectNode
|
queued[insertIndex] = effect :: EffectNode
|
||||||
|
|
@ -351,8 +509,10 @@ local function notify(effect: ReactiveNode)
|
||||||
until false
|
until false
|
||||||
|
|
||||||
queuedLength = insertIndex
|
queuedLength = insertIndex
|
||||||
|
|
||||||
insertIndex -= 1
|
insertIndex -= 1
|
||||||
|
|
||||||
|
-- Reverse the collected effects to maintain correct order
|
||||||
|
-- Note: insertIndex should decrement before the next condition evaluation
|
||||||
while firstInsertedIndex < insertIndex do
|
while firstInsertedIndex < insertIndex do
|
||||||
local left = queued[firstInsertedIndex]
|
local left = queued[firstInsertedIndex]
|
||||||
queued[firstInsertedIndex] = queued[insertIndex]
|
queued[firstInsertedIndex] = queued[insertIndex]
|
||||||
|
|
@ -362,6 +522,15 @@ local function notify(effect: ReactiveNode)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Called when a node is not being watched by any subscribers. Cleans up the
|
||||||
|
node's dependencies and performs the necessary cleanup operations:
|
||||||
|
|
||||||
|
- Computed signals: Unlink dependencies and mark as dirty for potential future updates
|
||||||
|
- Effects/scopes: Perform full cleanup to free resources
|
||||||
|
|
||||||
|
@param node Reactive node (computed or effect/scope)
|
||||||
|
]]
|
||||||
local function unwatched(node: ReactiveNode)
|
local function unwatched(node: ReactiveNode)
|
||||||
if bit32.band(node.flags, MUTABLE) == 0 then
|
if bit32.band(node.flags, MUTABLE) == 0 then
|
||||||
effectOper(node :: EffectScopeNode)()
|
effectOper(node :: EffectScopeNode)()
|
||||||
|
|
@ -376,10 +545,10 @@ end
|
||||||
Creates a reactive signal that stores a value and notifies subscribers when
|
Creates a reactive signal that stores a value and notifies subscribers when
|
||||||
the value changes. The signal provides both a getter and a setter.
|
the value changes. The signal provides both a getter and a setter.
|
||||||
|
|
||||||
@param initialValue The initial value of the signal.
|
@param initialValue Initial value for the signal
|
||||||
@param equals A comparator function to determine if the signal's value has changed.
|
@param equals Optional comparator function to check for value changes
|
||||||
@return A function to get the current value of the signal.
|
@return Getter function to read the signal value
|
||||||
@return A function to set a new value and notify subscribers.
|
@return Setter function to update the signal value
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
|
@see https://github.com/littensy/charm?tab=readme-ov-file#signalinitialValue-equals
|
||||||
]]
|
]]
|
||||||
local function signal<T>(
|
local function signal<T>(
|
||||||
|
|
@ -397,12 +566,12 @@ local function signal<T>(
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Creates a reactive atom that acts as both a signal getter and setter.
|
Creates a signal and returns a function that can act as both the getter and
|
||||||
Calling the atom with an argument sets its value, while calling it with no
|
setter. Calling the atom with one argument sets its value, while calling it
|
||||||
arguments returns its current value.
|
with no arguments returns its current value.
|
||||||
|
|
||||||
@param initialValue The initial value of the atom. If empty, the atom's value will be `nil`.
|
@param initialValue Initial value for the atom
|
||||||
@param equals A comparator function to determine if the atom's value has changed.
|
@param equals Optional comparator function to check for value changes
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#atominitialValue-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)?): Atom<T>
|
local function atom<T>(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): Atom<T>
|
||||||
|
|
@ -418,7 +587,8 @@ end
|
||||||
other signals. The computed signal's value is automatically updated when a
|
other signals. The computed signal's value is automatically updated when a
|
||||||
change is detected in any of its dependencies.
|
change is detected in any of its dependencies.
|
||||||
|
|
||||||
@param getter A function that produces the next value.
|
@param getter Function that calculates the next value
|
||||||
|
@return A function that returns the cached value
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
@see https://github.com/littensy/charm?tab=readme-ov-file#computedgetter
|
||||||
]]
|
]]
|
||||||
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
|
||||||
|
|
@ -439,73 +609,88 @@ end
|
||||||
If the effect is called within another effect, it will be disposed when the
|
If the effect is called within another effect, it will be disposed when the
|
||||||
parent effect re-runs.
|
parent effect re-runs.
|
||||||
|
|
||||||
@param fn The function to run when dependencies change.
|
@param fn Function to execute reactively
|
||||||
@return A function for disposing the effect.
|
@return A cleanup function that stops the effect
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
|
||||||
]]
|
]]
|
||||||
local function effect(fn: () -> ...(() -> ())?): () -> ()
|
local function effect(fn: () -> ...(() -> ())?): () -> ()
|
||||||
local e: EffectNode = {
|
local this: EffectNode = {
|
||||||
fn = noYield(fn),
|
fn = noYield(fn),
|
||||||
flags = bit32.bor(WATCHING, RECURSED_CHECK),
|
flags = bit32.bor(WATCHING, RECURSED_CHECK),
|
||||||
cleanups = {},
|
cleanups = {},
|
||||||
}
|
}
|
||||||
local prevSub = setActiveSub(e)
|
|
||||||
if prevSub and (flags.trackInnerEffects or prevSub.flags == NONE) then
|
if activeSub and (flags.trackInnerEffects or activeSub.flags == NONE) then
|
||||||
link(e, prevSub, 0)
|
link(this, activeSub, 0)
|
||||||
end
|
end
|
||||||
local success, result = pcall(e.fn)
|
|
||||||
|
local prevSub = setActiveSub(this)
|
||||||
|
local success, result = pcall(this.fn)
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
|
|
||||||
|
this.flags = bit32.band(this.flags, bit32.bnot(RECURSED_CHECK))
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(e.cleanups, noYield(result))
|
table.insert(this.cleanups, noYield(result))
|
||||||
end
|
end
|
||||||
return effectOper(e)
|
|
||||||
|
return effectOper(this)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Creates an effect scope that can capture reactive effects and computed
|
Creates an effect scope that can capture reactive effects and computed
|
||||||
signals created within it so that they can be disposed together.
|
signals created within it so that they can be disposed together.
|
||||||
|
|
||||||
@param fn A function that may create reactive effects.
|
@param fn Function that creates effects within the scope
|
||||||
@param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
|
@param detached If true, the scope will not be tracked by the parent effect/scope
|
||||||
@return A function for disposing all effects created within the scope.
|
@return A cleanup function that stops all effects in the scope
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
@see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
|
||||||
]]
|
]]
|
||||||
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
|
||||||
local e: EffectScopeNode = {
|
local this: EffectScopeNode = {
|
||||||
flags = NONE,
|
flags = NONE,
|
||||||
cleanups = {},
|
cleanups = {},
|
||||||
}
|
}
|
||||||
local prevSub = setActiveSub(e)
|
|
||||||
if prevSub and not detached then
|
if activeSub and not detached then
|
||||||
link(e, prevSub, 0)
|
link(this, activeSub, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local prevSub = setActiveSub(this)
|
||||||
local success, result = pcall(noYield(fn))
|
local success, result = pcall(noYield(fn))
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
if not success then
|
if not success then
|
||||||
error(result)
|
error(result)
|
||||||
elseif result then
|
elseif result then
|
||||||
table.insert(e.cleanups, noYield(result))
|
table.insert(this.cleanups, noYield(result))
|
||||||
end
|
end
|
||||||
return effectOper(e)
|
|
||||||
|
return effectOper(this)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Allows you to manually trigger updates for downstream dependencies when
|
Allows you to manually trigger updates for downstream dependencies when
|
||||||
you've directly mutated a signal's value without using the signal setter.
|
you've directly mutated a signal's value without using the signal setter.
|
||||||
|
|
||||||
@param fn The dependency or a function that calls multiple dependencies.
|
@param fn A signal or function that accesses signals
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#triggercallback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#triggercallback
|
||||||
]]
|
]]
|
||||||
local function trigger(fn: () -> ())
|
local function trigger(fn: () -> ())
|
||||||
local sub: ReactiveNode = {
|
local sub: ReactiveNode = {
|
||||||
flags = WATCHING,
|
flags = WATCHING,
|
||||||
}
|
}
|
||||||
|
|
||||||
local prevSub = setActiveSub(sub)
|
local prevSub = setActiveSub(sub)
|
||||||
local success, result: any = pcall(noYield(fn))
|
local success, result: any = pcall(noYield(fn))
|
||||||
activeSub = prevSub
|
activeSub = prevSub
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
error(result)
|
||||||
|
end
|
||||||
|
|
||||||
local link = sub.deps
|
local link = sub.deps
|
||||||
while link do
|
while link do
|
||||||
local dep = link.dep
|
local dep = link.dep
|
||||||
|
|
@ -517,12 +702,10 @@ local function trigger(fn: () -> ())
|
||||||
shallowPropagate(subs)
|
shallowPropagate(subs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if batchDepth == 0 then
|
if batchDepth == 0 then
|
||||||
flush()
|
flush()
|
||||||
end
|
end
|
||||||
if not success then
|
|
||||||
error(result)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
|
|
@ -530,8 +713,8 @@ end
|
||||||
there is no active effect, a warning is logged, unless `failSilently` is
|
there is no active effect, a warning is logged, unless `failSilently` is
|
||||||
set to `true`.
|
set to `true`.
|
||||||
|
|
||||||
@param fn The cleanup function to register.
|
@param fn Function to run during cleanup
|
||||||
@param failSilently If `true`, suppresses the warning when there is no active effect or scope. Defaults to `false`.
|
@param failSilently If true, silences the warning when called outside an effect/scope
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
@see https://github.com/littensy/charm?tab=readme-ov-file#oncleanupcallback-failsilently
|
||||||
]]
|
]]
|
||||||
local function onCleanup(fn: () -> (), failSilently: boolean?)
|
local function onCleanup(fn: () -> (), failSilently: boolean?)
|
||||||
|
|
@ -548,9 +731,9 @@ end
|
||||||
|
|
||||||
For an alternative that still cleans up inner effects, use `peek` instead.
|
For an alternative that still cleans up inner effects, use `peek` instead.
|
||||||
|
|
||||||
@param fn A function that may read signals or create effects.
|
@param fn A signal or function that accesses signals
|
||||||
@param ...args Arguments to pass to the function.
|
@param ...args Arguments to pass to the function
|
||||||
@return The return value of the function.
|
@return Values returned by the function
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#untrackedcallback
|
||||||
]]
|
]]
|
||||||
local function untracked<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
local function untracked<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
|
@ -573,9 +756,9 @@ end
|
||||||
|
|
||||||
To avoid capturing inner effects, use `untracked` instead.
|
To avoid capturing inner effects, use `untracked` instead.
|
||||||
|
|
||||||
@param fn A function that may read signals.
|
@param fn A signal or function that accesses signals
|
||||||
@param ...args Arguments to pass to the function.
|
@param ...args Arguments to pass to the function
|
||||||
@return The return value of the function.
|
@return Values returned by the function
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#peekcallback
|
||||||
]]
|
]]
|
||||||
local function peek<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
local function peek<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
|
@ -600,9 +783,9 @@ end
|
||||||
Combines multiple updates into a single "commit" that runs effects and
|
Combines multiple updates into a single "commit" that runs effects and
|
||||||
computed signals after the provided callback finishes running.
|
computed signals after the provided callback finishes running.
|
||||||
|
|
||||||
@param fn A function that performs multiple updates.
|
@param fn Function that updates signals
|
||||||
@param ...args Arguments to pass to the function.
|
@param ...args Arguments to pass to the function
|
||||||
@return The return value of the function.
|
@return Values returned by the function
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#batchcallback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#batchcallback
|
||||||
]]
|
]]
|
||||||
local function batch<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
local function batch<Args..., Results...>(fn: (Args...) -> Results..., ...: Args...): Results...
|
||||||
|
|
@ -620,8 +803,8 @@ end
|
||||||
signal, it is returned as-is. Useful for memoizing a getter function
|
signal, it is returned as-is. Useful for memoizing a getter function
|
||||||
without creating unnecessary nested signals.
|
without creating unnecessary nested signals.
|
||||||
|
|
||||||
@param fn A function that returns the value to track.
|
@param fn A signal or function that accesses signals
|
||||||
@return A computed signal that tracks the value returned by the getter.
|
@return Computed signal if `fn` is not already a signal
|
||||||
]]
|
]]
|
||||||
local function toSignal<T>(fn: () -> T): () -> T
|
local function toSignal<T>(fn: () -> T): () -> T
|
||||||
local name = debug.info(fn, "n")
|
local name = debug.info(fn, "n")
|
||||||
|
|
@ -636,9 +819,9 @@ end
|
||||||
Effects created within the callback are cleaned up when the listener runs
|
Effects created within the callback are cleaned up when the listener runs
|
||||||
or the listener is disposed.
|
or the listener is disposed.
|
||||||
|
|
||||||
@param getter A function that returns the value to watch.
|
@param getter A signal or function that accesses signals
|
||||||
@param fn The function to run when the value changes.
|
@param fn Function to run when the value changes
|
||||||
@return A function for disposing the effect.
|
@return A cleanup function that stops the effect
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#listengetter-callback
|
||||||
]]
|
]]
|
||||||
local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): () -> ()
|
local function listen<T>(getter: () -> T, fn: (state: T, previous: T?) -> ()): () -> ()
|
||||||
|
|
@ -660,9 +843,9 @@ end
|
||||||
Effects created within the callback are cleaned up when the listener runs
|
Effects created within the callback are cleaned up when the listener runs
|
||||||
or the listener is disposed.
|
or the listener is disposed.
|
||||||
|
|
||||||
@param getter A function that returns the value to watch.
|
@param getter A signal or function that accesses signals
|
||||||
@param fn The function to run when the value changes.
|
@param fn Function to run when the value changes
|
||||||
@return A function for disposing the effect.
|
@return A cleanup function that stops the effect
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#subscribegetter-callback
|
||||||
]]
|
]]
|
||||||
local function subscribe<T>(getter: () -> T, fn: (state: T, lastState: T) -> ()): () -> ()
|
local function subscribe<T>(getter: () -> T, fn: (state: T, lastState: T) -> ()): () -> ()
|
||||||
|
|
@ -710,9 +893,9 @@ end
|
||||||
key is removed or the `observe()` effect is disposed. Effects created
|
key is removed or the `observe()` effect is disposed. Effects created
|
||||||
during the callback will also be bound to the lifetime of the key.
|
during the callback will also be bound to the lifetime of the key.
|
||||||
|
|
||||||
@param getter A function that returns the table to observe.
|
@param getter Function that returns a table
|
||||||
@param fn A function that is called for each key in the table.
|
@param fn Function that is called for each key added to the table
|
||||||
@return A function for disposing the observer.
|
@return A cleanup function that disposes scopes for all keys
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
@see https://github.com/littensy/charm?tab=readme-ov-file#observegetter-callback
|
||||||
]]
|
]]
|
||||||
local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
local function observe<K, V>(getter: () -> { [K]: V }, fn: (item: V, key: K) -> ...(() -> ())?): () -> ()
|
||||||
|
|
@ -772,8 +955,8 @@ end
|
||||||
of an existing atom. If the key is omitted from the mapper's return value,
|
of an existing atom. If the key is omitted from the mapper's return value,
|
||||||
the original key is preserved.
|
the original key is preserved.
|
||||||
|
|
||||||
@param getter Returns the table to map over.
|
@param getter Function that returns a table
|
||||||
@param mapper Transforms each value-key pair from the source into a new value or value-key pair for the resulting table.
|
@param mapper Function that maps each entry from the source to a new entry in the mapped table
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
@see https://github.com/littensy/charm?tab=readme-ov-file#mappedgetter-mapper
|
||||||
]]
|
]]
|
||||||
local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)): () -> { [KO]: VO }
|
||||||
|
|
@ -834,8 +1017,6 @@ end
|
||||||
--[[
|
--[[
|
||||||
Determines if the provided value is a reactive proxy created by `reactive()`.
|
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
|
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
]]
|
]]
|
||||||
local function isReactive(value: any): boolean
|
local function isReactive(value: any): boolean
|
||||||
|
|
@ -846,8 +1027,8 @@ 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.
|
||||||
|
|
||||||
@param value A reactive proxy or a non-proxy value.
|
@param value A reactive proxy or a non-proxy value
|
||||||
@return The unwrapped target value.
|
@return The source table if `value` is a reactive proxy
|
||||||
@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
|
||||||
|
|
@ -880,9 +1061,9 @@ end
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
@param initialValue The value to make deeply reactive.
|
@param initialValue Source table to make deeply reactive
|
||||||
@return A proxy table that tracks reads to nested properties.
|
@return A proxy table that tracks reads to nested properties
|
||||||
@return A function for updating the original table.
|
@return A function for batching mutations to the source table
|
||||||
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
@see https://github.com/littensy/charm?tab=readme-ov-file#reactiveinitialvalue
|
||||||
]]
|
]]
|
||||||
local function reactive<T>(initialValue: T & {}, trackParent: (() -> ())?): (T, (update: ((T) -> ())?) -> ())
|
local function reactive<T>(initialValue: T & {}, trackParent: (() -> ())?): (T, (update: ((T) -> ())?) -> ())
|
||||||
|
|
@ -926,9 +1107,9 @@ end
|
||||||
Wraps a value in `reactive()` if it is a table; otherwise, returns the
|
Wraps a value in `reactive()` if it is a table; otherwise, returns the
|
||||||
value as-is.
|
value as-is.
|
||||||
|
|
||||||
@param value A value to make reactive.
|
@param value Source value to make deeply reactive
|
||||||
@param trackParent An optional tracking function to propagate deep updates.
|
@param trackParent Optional tracking function to propagate deep updates
|
||||||
@return A reactive proxy if the value is a table.
|
@return A reactive proxy if the value is a table
|
||||||
]]
|
]]
|
||||||
local function toReactive<T>(value: T, trackParent: (() -> ())?): T
|
local function toReactive<T>(value: T, trackParent: (() -> ())?): T
|
||||||
return if type(value) == "table" then reactive(value, trackParent) else value
|
return if type(value) == "table" then reactive(value, trackParent) else value
|
||||||
|
|
@ -974,30 +1155,22 @@ function ReactiveProxy.__len(self: ReactiveProxy)
|
||||||
return #self.__target
|
return #self.__target
|
||||||
end
|
end
|
||||||
|
|
||||||
local reactiveSystem = system.createReactiveSystem({
|
link, unlink, propagate, checkDirty, shallowPropagate = system.createReactiveSystem({
|
||||||
update = update,
|
update = update,
|
||||||
notify = notify,
|
notify = notify,
|
||||||
unwatched = unwatched,
|
unwatched = unwatched,
|
||||||
})
|
})
|
||||||
|
|
||||||
link = reactiveSystem.link
|
|
||||||
unlink = reactiveSystem.unlink
|
|
||||||
propagate = reactiveSystem.propagate
|
|
||||||
checkDirty = reactiveSystem.checkDirty
|
|
||||||
shallowPropagate = reactiveSystem.shallowPropagate
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ReactiveFlags = system.ReactiveFlags,
|
-- Core reactive primitives
|
||||||
flags = flags,
|
|
||||||
getActiveSub = getActiveSub,
|
|
||||||
setActiveSub = setActiveSub,
|
|
||||||
startBatch = startBatch,
|
|
||||||
endBatch = endBatch,
|
|
||||||
signal = signal,
|
signal = signal,
|
||||||
computed = computed,
|
computed = computed,
|
||||||
effect = effect,
|
effect = effect,
|
||||||
effectScope = effectScope,
|
effectScope = effectScope,
|
||||||
trigger = trigger,
|
trigger = trigger,
|
||||||
|
|
||||||
|
-- Charm utilities
|
||||||
|
flags = flags,
|
||||||
atom = atom,
|
atom = atom,
|
||||||
onCleanup = onCleanup,
|
onCleanup = onCleanup,
|
||||||
untracked = untracked,
|
untracked = untracked,
|
||||||
|
|
@ -1011,7 +1184,16 @@ return {
|
||||||
getter: () -> { [KI]: VI },
|
getter: () -> { [KI]: VI },
|
||||||
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 proxies
|
||||||
reactive = reactive,
|
reactive = reactive,
|
||||||
isReactive = isReactive,
|
isReactive = isReactive,
|
||||||
toRaw = toRaw,
|
toRaw = toRaw,
|
||||||
|
|
||||||
|
-- Advanced API
|
||||||
|
ReactiveFlags = system.ReactiveFlags,
|
||||||
|
getActiveSub = getActiveSub,
|
||||||
|
setActiveSub = setActiveSub,
|
||||||
|
startBatch = startBatch,
|
||||||
|
endBatch = endBatch,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,6 @@
|
||||||
--[[
|
--[[
|
||||||
Reactive system adapted from alien-signals at
|
Reactive system adapted from alien-signals at
|
||||||
https://github.com/stackblitz/alien-signals/blob/master/src/system.ts
|
https://github.com/stackblitz/alien-signals/blob/master/src/system.ts
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024-present Johnson Chu
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
export type ReactiveNode = {
|
export type ReactiveNode = {
|
||||||
|
|
@ -249,13 +227,7 @@ local function createReactiveSystem(props: {
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return link, unlink, propagate, checkDirty, shallowPropagate
|
||||||
link = link,
|
|
||||||
unlink = unlink,
|
|
||||||
propagate = propagate,
|
|
||||||
checkDirty = checkDirty,
|
|
||||||
shallowPropagate = shallowPropagate,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@ rojo = "rojo-rbx/rojo@7.6.1"
|
||||||
stylua = "johnnymorganz/stylua@2.3.1"
|
stylua = "johnnymorganz/stylua@2.3.1"
|
||||||
selene = "Kampfkarren/selene@0.29.0"
|
selene = "Kampfkarren/selene@0.29.0"
|
||||||
luau-lsp = "johnnymorganz/luau-lsp@1.57.1"
|
luau-lsp = "johnnymorganz/luau-lsp@1.57.1"
|
||||||
lute = "luau-lang/lute@0.1.0-nightly.20251127"
|
lute = "luau-lang/lute@0.1.0-nightly.20251211"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue