refactor: rewrite untracked, update alien-signals

This commit is contained in:
littensy 2025-09-23 16:50:39 -07:00
parent a8051257e5
commit cb52ece4d3
15 changed files with 239 additions and 114 deletions

View file

@ -4,6 +4,7 @@ local bench = require("./bench")
local atom = Charm.atom local atom = Charm.atom
local computed = Charm.computed local computed = Charm.computed
local effect = Charm.effect local effect = Charm.effect
local listen = Charm.listen
for _, w in { 10, 100 } do for _, w in { 10, 100 } do
for _, h in { 10, 100 } do for _, h in { 10, 100 } do
@ -25,3 +26,22 @@ for _, w in { 10, 100 } do
end) end)
end end
end end
for _, w in { 10, 100 } do
for _, h in { 10, 100 } do
bench.run(`propagate (listen) {w}*{h}`, function()
local src = atom(1)
for _ = 1, w do
local last = src
for _ = 1, h do
local prev = last
last = computed(function()
return prev() + 1
end)
end
listen(last, function() end)
end
src(src() + 1)
end)
end
end

View file

@ -7,7 +7,7 @@
"publish:npm": "pnpm publish -r --access public", "publish:npm": "pnpm publish -r --access public",
"publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally" "publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally"
}, },
"packageManager": "pnpm@10.12.4", "packageManager": "pnpm@10.17.1",
"devDependencies": { "devDependencies": {
"@rbxts-js/react": "17.1.20-ts.1", "@rbxts-js/react": "17.1.20-ts.1",
"@rbxts-js/react-roblox": "17.1.20-ts.1", "@rbxts-js/react-roblox": "17.1.20-ts.1",

View file

@ -4,13 +4,15 @@ type Atom<T> = Charm.Atom<T>
--- A payload that can be sent from the server to the client to synchronize --- A payload that can be sent from the server to the client to synchronize
--- state between the two. --- state between the two.
export type SyncPayload = { export type SyncPayload =
type: "init", | {
data: { [string | number]: any }, type: "init",
} | { data: { [string | number]: any },
type: "patch", }
data: { [string | number]: any }, | {
} type: "patch",
data: { [string | number]: any },
}
export type AtomMap = { export type AtomMap = {
[string | number]: Atom<any>, [string | number]: Atom<any>,

View file

@ -356,4 +356,25 @@ test("should run cleanups outside any evaluation context", function()
assert(calls == 0, "should not run after b update") assert(calls == 0, "should not run after b update")
end) end)
test("should allow self cleanup", function()
local calls = 0
local cleanups = 0
local a = atom(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")
a(1)
assert(calls == 1, "should not call effect after a update")
assert(cleanups == 1, "should not call cleanup after a update")
end)
return {} return {}

View file

@ -4,6 +4,7 @@ local suite = require("../../../../test/suite")
local atom = Charm.atom local atom = Charm.atom
local listen = Charm.listen local listen = Charm.listen
local effect = Charm.effect local effect = Charm.effect
local onCleanup = Charm.onCleanup
local test = suite.test local test = suite.test
test("should listen to selector", function() test("should listen to selector", function()
@ -115,4 +116,24 @@ test("should skip redundant updates", function()
assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`) assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`)
end) end)
test("should allow self cleanup", function()
local calls = 0
local cleanups = 0
local a = atom(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")
a(1)
assert(calls == 1, "should not call effect after a update")
assert(cleanups == 1, "should not call cleanup after a update")
end)
return {} return {}

View file

@ -61,4 +61,36 @@ test("should dispose scopes", function()
assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`) assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`)
end) end)
test("should queue addition", function()
local items = atom({ 1 })
local edits = {}
observe(items, function(item)
items({ 1, 2 })
items({ 1, 2, 3, 4 })
table.insert(edits, `+{item}`)
return function()
table.insert(edits, `-{item}`)
end
end)
assert(table.concat(edits) == "+1+2+3+4", `bad edits: {table.concat(edits)}`)
end)
test("should queue removal", function()
local items = atom({ 1, 2, 3, 4 })
local edits = {}
observe(items, function(item)
items({ 1, 2, 3 })
items({ 1 })
table.insert(edits, `+{item}`)
return function()
table.insert(edits, `-{item}`)
end
end)
assert(table.concat(edits) == "+1+2+3+4-2-3-4", `bad edits: {table.concat(edits)}`)
end)
return {} return {}

View file

@ -105,4 +105,26 @@ test("should block tracking inside computed atoms", function()
assert(c() == 3, "c should be 3") assert(c() == 3, "c should be 3")
end) end)
test("should track outside untracked", function()
local a = atom(1)
local b = atom(2)
local c = atom(3)
local spyCalls = 0
effect(function()
a()
untracked(c)
b()
spyCalls += 1
end)
assert(spyCalls == 1, "should be called once")
a(10)
assert(spyCalls == 2, "should be called twice")
b(20)
assert(spyCalls == 3, "should be called three times")
c(30)
assert(spyCalls == 3, "should not run on c change")
end)
return {} return {}

View file

@ -1,7 +1,10 @@
local core = require("./core") local core = require("./core")
local flags = require("./flags")
local utils = require("./utils")
local startBatch = core.startBatch local startBatch = core.startBatch
local endBatch = core.endBatch local endBatch = core.endBatch
local noYield = utils.noYield
local function handleBatched(ok: boolean, ...) local function handleBatched(ok: boolean, ...)
endBatch() endBatch()
@ -14,7 +17,7 @@ end
local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results... local function batched<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
startBatch() startBatch()
return handleBatched(pcall(callback, ...)) return handleBatched((if flags.strict then noYield else pcall)(callback, ...))
end end
return batched return batched

View file

@ -21,14 +21,16 @@ local WATCHING = 0b0000010
local DIRTY = 0b0010000 local DIRTY = 0b0010000
local PENDING = 0b0100000 local PENDING = 0b0100000
local QUEUED = 0b1000000 local QUEUED = 0b1000000
local UNTRACKED = 0b10000000
type EffectScope = ReactiveNode & { type EffectScope = ReactiveNode & {
cleanups: { () -> () }, cleanups: { () -> () },
} }
type Effect = ReactiveNode & { type Effect = ReactiveNode & {
fn: () -> ...(() -> ())?, fn: (dispose: () -> ()) -> ...(() -> ())?,
cleanups: { () -> () }, cleanups: { () -> () },
cleanup: () -> (),
} }
type Computed<T = any> = ReactiveNode & { type Computed<T = any> = ReactiveNode & {
@ -59,7 +61,6 @@ local notifyIndex = 0
local activeScope: Effect | EffectScope? local activeScope: Effect | EffectScope?
local activeSub: ReactiveNode? local activeSub: ReactiveNode?
local activeSubWatching = true
local function getCurrentSub(): ReactiveNode? local function getCurrentSub(): ReactiveNode?
return activeSub return activeSub
@ -81,16 +82,10 @@ local function setCurrentScope(scope: Effect | EffectScope?)
return prevScope return prevScope
end end
local function setCurrentWatching(watching: boolean): boolean
local prevSubWatching = activeSubWatching
activeSubWatching = watching
return prevSubWatching
end
local function runCleanups(effect: EffectScope) local function runCleanups(effect: EffectScope)
for _, cleanup in effect.cleanups do for _, cleanup in effect.cleanups do
local prev = setCurrentSub(nil) local prev = setCurrentSub(nil)
local success, result: any = (config.strict and noYield or pcall)(cleanup) local success, result: any = (if config.strict then noYield else pcall)(cleanup)
setCurrentSub(prev) setCurrentSub(prev)
if not success then if not success then
error(result) error(result)
@ -113,12 +108,10 @@ local function run(effect: Effect, flags: ReactiveFlags): ()
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
runCleanups(effect) runCleanups(effect)
local prev = setCurrentSub(effect) local prev = setCurrentSub(effect)
local prevTracking = setCurrentWatching(true)
startTracking(effect) startTracking(effect)
local success, result: (() -> ())? = (config.strict and noYield or pcall)(effect.fn) local success, result: (() -> ())? = (if config.strict then noYield else pcall)(effect.fn, effect.cleanup)
setCurrentSub(prev) setCurrentSub(prev)
endTracking(effect) endTracking(effect)
setCurrentWatching(prevTracking)
if not success then if not success then
error(result) error(result)
elseif result then elseif result then
@ -170,13 +163,11 @@ end
local function updateComputed(computed: Computed): boolean local function updateComputed(computed: Computed): boolean
local prevSub = setCurrentSub(computed) local prevSub = setCurrentSub(computed)
local prevWatching = setCurrentWatching(true)
startTracking(computed) startTracking(computed)
local oldValue = computed.value local oldValue = computed.value
local success, result = (config.strict and noYield or pcall)(computed.getter, oldValue) local success, result = (if config.strict then noYield else pcall)(computed.getter, oldValue)
setCurrentSub(prevSub) setCurrentSub(prevSub)
endTracking(computed) endTracking(computed)
setCurrentWatching(prevWatching)
if not success then if not success then
error(result) error(result)
end end
@ -209,7 +200,7 @@ local function readAtom<T>(atom: Atom<T>): T
end end
end end
end end
if activeSub and activeSubWatching then if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then
link(atom, activeSub) link(atom, activeSub)
end end
return value return value
@ -244,7 +235,7 @@ local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
local newValue = ... local newValue = ...
if type(newValue) == "function" then if type(newValue) == "function" then
local prev = setCurrentSub(nil) local prev = setCurrentSub(nil)
local success, result = (config.strict and noYield or pcall)(newValue, oldValue) local success, result = (if config.strict then noYield else pcall)(newValue, oldValue)
setCurrentSub(prev) setCurrentSub(prev)
if not success then if not success then
error(result) error(result)
@ -270,7 +261,7 @@ local function computedGetter<T>(computed: Computed<T>): () -> T
elseif bit32.btest(flags, PENDING) then elseif bit32.btest(flags, PENDING) then
computed.flags = bit32.band(flags, bit32.bnot(PENDING)) computed.flags = bit32.band(flags, bit32.bnot(PENDING))
end end
if activeSub and activeSubWatching then if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then
link(computed, activeSub) link(computed, activeSub)
elseif activeScope then elseif activeScope then
link(computed, activeScope) link(computed, activeScope)
@ -350,28 +341,28 @@ local function computed<T>(getter: (previousValue: T?) -> T, equals: ((T, T) ->
}) })
end end
local function effect(fn: () -> ...(() -> ())?): () -> () local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
local self: Effect = { local self: Effect = {
fn = fn, fn = fn,
flags = WATCHING, flags = WATCHING,
cleanups = {}, cleanups = {},
cleanup = nil :: any,
} }
self.cleanup = effectStop(self)
if activeSub then if activeSub then
link(self, activeSub) link(self, activeSub)
elseif activeScope then elseif activeScope then
link(self, activeScope) link(self, activeScope)
end end
local prev = setCurrentSub(self) local prev = setCurrentSub(self)
local prevWatching = setCurrentWatching(true) local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn, self.cleanup)
local success, result: (() -> ())? = (config.strict and noYield or pcall)(fn)
setCurrentSub(prev) setCurrentSub(prev)
setCurrentWatching(prevWatching)
if not success then if not success then
error(result) error(result)
elseif result then elseif result then
table.insert(self.cleanups, result) table.insert(self.cleanups, result)
end end
return effectStop(self) return self.cleanup
end end
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> () local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
@ -384,7 +375,7 @@ local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () ->
end end
local prevSub = setCurrentSub(nil) local prevSub = setCurrentSub(nil)
local prevScope = setCurrentScope(self) local prevScope = setCurrentScope(self)
local success, result: (() -> ())? = (config.strict and noYield or pcall)(fn) local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn)
setCurrentScope(prevScope) setCurrentScope(prevScope)
setCurrentSub(prevSub) setCurrentSub(prevSub)
if not success then if not success then
@ -416,7 +407,6 @@ return {
setCurrentSub = setCurrentSub, setCurrentSub = setCurrentSub,
getCurrentScope = getCurrentScope, getCurrentScope = getCurrentScope,
setCurrentScope = setCurrentScope, setCurrentScope = setCurrentScope,
setCurrentWatching = setCurrentWatching,
onCleanup = onCleanup, onCleanup = onCleanup,
getBatchDepth = getBatchDepth, getBatchDepth = getBatchDepth,
startBatch = startBatch, startBatch = startBatch,

View file

@ -1,3 +1,5 @@
--!nolint DeprecatedApi
local batched = require("@self/batched") local batched = require("@self/batched")
local core = require("@self/core") local core = require("@self/core")
local flags = require("@self/flags") local flags = require("@self/flags")

View file

@ -3,6 +3,8 @@ local core = require("./core")
local effect = core.effect local effect = core.effect
local effectScope = core.effectScope local effectScope = core.effectScope
local onCleanup = core.onCleanup local onCleanup = core.onCleanup
local startBatch = core.startBatch
local endBatch = core.endBatch
local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> () local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> ()
local scopes: { [K]: () -> () } = {} local scopes: { [K]: () -> () } = {}
@ -18,7 +20,7 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
end end
for key, value in currentItems do for key, value in currentItems do
if previousItems[key] == nil then if previousItems[key] == nil and scopes[key] == nil then
scopes[key] = effectScope(function() scopes[key] = effectScope(function()
return observer(value, key) return observer(value, key)
end, true) end, true)
@ -37,7 +39,9 @@ local function observe<K, V>(selector: () -> { [K]: V }, observer: (item: V, key
local previousItems = currentItems local previousItems = currentItems
currentItems = selector() currentItems = selector()
if currentItems ~= previousItems then if currentItems ~= previousItems then
startBatch()
updateScopes(currentItems, previousItems) updateScopes(currentItems, previousItems)
endBatch()
end end
end) end)

View file

@ -11,6 +11,7 @@ export type ReactiveNode = {
} }
export type Link = { export type Link = {
version: number,
dep: ReactiveNode, dep: ReactiveNode,
sub: ReactiveNode, sub: ReactiveNode,
prevSub: Link?, prevSub: Link?,
@ -26,13 +27,14 @@ type Stack<T> = {
export type ReactiveFlags = number export type ReactiveFlags = number
local NONE = 0b0000000 local NONE = 0b00000000
local MUTABLE = 0b0000001 local MUTABLE = 0b00000001
local WATCHING = 0b0000010 local WATCHING = 0b00000010
local RECURSED_CHECK = 0b0000100 local RECURSED_CHECK = 0b00000100
local RECURSED = 0b0001000 local RECURSED = 0b00001000
local DIRTY = 0b0010000 local DIRTY = 0b00010000
local PENDING = 0b0100000 local PENDING = 0b00100000
local UNTRACKED = 0b10000000
local ReactiveFlags = { local ReactiveFlags = {
None = NONE, None = NONE,
@ -42,6 +44,7 @@ local ReactiveFlags = {
Recursed = RECURSED, Recursed = RECURSED,
Dirty = DIRTY, Dirty = DIRTY,
Pending = PENDING, Pending = PENDING,
Untracked = UNTRACKED,
} }
local function createReactiveSystem(props: { local function createReactiveSystem(props: {
@ -52,20 +55,15 @@ local function createReactiveSystem(props: {
local update = props.update local update = props.update
local notify = props.notify local notify = props.notify
local unwatched = props.unwatched local unwatched = props.unwatched
local currentVersion = 0
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
local depsTail = sub.depsTail local link = sub.depsTail
if depsTail then while link do
local link = sub.deps :: Link if link == checkLink then
repeat return true
if link == checkLink then end
return true link = link.prevDep
end
if link == depsTail then
break
end
link = link.nextDep :: Link
until not link
end end
return false return false
end end
@ -75,20 +73,18 @@ local function createReactiveSystem(props: {
if prevDep and prevDep.dep == dep then if prevDep and prevDep.dep == dep then
return return
end end
local nextDep: Link? local nextDep = if prevDep then prevDep.nextDep else sub.deps
local recursedCheck = bit32.btest(sub.flags, RECURSED_CHECK) if nextDep and nextDep.dep == dep then
if recursedCheck then nextDep.version = currentVersion
nextDep = if prevDep then prevDep.nextDep else sub.deps sub.depsTail = nextDep
if nextDep and nextDep.dep == dep then return
sub.depsTail = nextDep
return
end
end end
local prevSub = dep.subsTail local prevSub = dep.subsTail
if prevSub and prevSub.sub == sub and (not recursedCheck or isValidLink(prevSub, sub)) then if prevSub and prevSub.version == currentVersion and prevSub.sub == sub then
return return
end end
local newLink: Link = { local newLink: Link = {
version = currentVersion,
dep = dep, dep = dep,
sub = sub, sub = sub,
prevDep = prevDep, prevDep = prevDep,
@ -154,35 +150,34 @@ local function createReactiveSystem(props: {
local flags = sub.flags local flags = sub.flags
if bit32.btest(flags, bit32.bor(MUTABLE, WATCHING)) then if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then
if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then sub.flags = bit32.bor(flags, PENDING)
sub.flags = bit32.bor(flags, PENDING) elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then
elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then flags = NONE
flags = NONE elseif not bit32.btest(flags, RECURSED_CHECK) then
elseif not bit32.btest(flags, RECURSED_CHECK) then sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING)
sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING) elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then
elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING))
sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING)) flags = bit32.band(flags, MUTABLE)
flags = bit32.band(flags, MUTABLE) else
else -- selene: allow(if_same_then_else)
-- selene: allow(if_same_then_else) flags = NONE
flags = NONE end
end
if bit32.btest(flags, WATCHING) then if bit32.btest(flags, WATCHING) then
notify(sub) notify(sub)
end end
if bit32.btest(flags, MUTABLE) then if bit32.btest(flags, MUTABLE) then
local subSubs = sub.subs local subSubs = sub.subs
if subSubs then if subSubs then
link = subSubs link = subSubs
if subSubs.nextSub then local nextSub = link.nextSub
stack = { value = next, prev = stack } if nextSub then
next = link.nextSub stack = { value = next, prev = stack }
end next = nextSub
continue
end end
continue
end end
end end
@ -214,6 +209,7 @@ local function createReactiveSystem(props: {
end end
local function startTracking(sub: ReactiveNode): () local function startTracking(sub: ReactiveNode): ()
currentVersion += 1
sub.depsTail = nil sub.depsTail = nil
sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK) sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK)
end end
@ -230,10 +226,10 @@ local function createReactiveSystem(props: {
local function shallowPropagate(link: Link): () local function shallowPropagate(link: Link): ()
repeat repeat
local sub = link.sub local sub = link.sub
local subFlags = sub.flags local flags = sub.flags
if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then if bit32.band(flags, bit32.bor(PENDING, DIRTY)) == PENDING then
sub.flags = bit32.bor(subFlags, DIRTY) sub.flags = bit32.bor(flags, DIRTY)
if bit32.btest(subFlags, WATCHING) then if bit32.btest(flags, WATCHING) then
notify(sub) notify(sub)
end end
end end
@ -244,16 +240,15 @@ local function createReactiveSystem(props: {
local function checkDirty(link: Link, sub: ReactiveNode): boolean local function checkDirty(link: Link, sub: ReactiveNode): boolean
local stack: Stack<Link>? local stack: Stack<Link>?
local checkDepth = 0 local checkDepth = 0
local dirty = false
repeat repeat
local dep = link.dep local dep = link.dep
local depFlags = dep.flags local flags = dep.flags
local dirty = false
if bit32.btest(sub.flags, DIRTY) then if bit32.btest(sub.flags, DIRTY) then
dirty = true dirty = true
elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then elseif bit32.band(flags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then
if update(dep) then if update(dep) then
local subs = dep.subs :: Link local subs = dep.subs :: Link
if subs.nextSub then if subs.nextSub then
@ -261,7 +256,7 @@ local function createReactiveSystem(props: {
end end
dirty = true dirty = true
end end
elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then elseif bit32.band(flags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then
if link.nextSub or link.prevSub then if link.nextSub or link.prevSub then
stack = { value = link, prev = stack } stack = { value = link, prev = stack }
end end
@ -271,9 +266,12 @@ local function createReactiveSystem(props: {
continue continue
end end
if not dirty and link.nextDep then if not dirty then
link = link.nextDep local nextDep = link.nextDep
continue if nextDep then
link = nextDep
continue
end
end end
local continueTop = false local continueTop = false
@ -296,16 +294,17 @@ local function createReactiveSystem(props: {
sub = link.sub sub = link.sub
continue continue
end end
dirty = false
else else
sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING)) sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING))
end end
sub = link.sub sub = link.sub
if link.nextDep then local nextDep = link.nextDep
link = link.nextDep if nextDep then
link = nextDep
continueTop = true continueTop = true
break break
end end
dirty = false
end end
if continueTop then if continueTop then

View file

@ -1,9 +1,12 @@
local core = require("./core") local core = require("./core")
local setCurrentWatching = core.setCurrentWatching local UNTRACKED = core.ReactiveFlags.Untracked
local getCurrentSub = core.getCurrentSub
local function handleUntracked(prevWatching: boolean, ok: boolean, ...) local function handleUntracked(sub: core.ReactiveNode?, ok: boolean, ...)
setCurrentWatching(prevWatching) if sub then
sub.flags = bit32.band(sub.flags, bit32.bnot(UNTRACKED))
end
if ok then if ok then
return ... return ...
else else
@ -12,7 +15,13 @@ local function handleUntracked(prevWatching: boolean, ok: boolean, ...)
end end
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results... local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
return handleUntracked(setCurrentWatching(false), pcall(callback, ...)) local sub = getCurrentSub()
if not sub or bit32.btest(sub.flags, UNTRACKED) then
return callback(...)
else
sub.flags = bit32.bor(sub.flags, UNTRACKED)
end
return handleUntracked(sub, pcall(callback, ...))
end end
return untracked return untracked

View file

@ -6,8 +6,8 @@
[tools] [tools]
wally = "UpliftGames/wally@0.3.2" wally = "UpliftGames/wally@0.3.2"
rojo = "rojo-rbx/rojo@7.5.1" rojo = "rojo-rbx/rojo@7.5.1"
stylua = "johnnymorganz/stylua@2.1.0" stylua = "johnnymorganz/stylua@2.2.0"
selene = "Kampfkarren/selene@0.28.0" selene = "Kampfkarren/selene@0.29.0"
luau-lsp = "johnnymorganz/luau-lsp@1.51.0" luau-lsp = "johnnymorganz/luau-lsp@1.53.5"
lune = "lune-org/lune@0.9.4" lune = "lune-org/lune@0.10.2"
lute = "luau-lang/lute@0.1.0-nightly.20250701" lute = "luau-lang/lute@0.1.0-nightly.20250923"

View file

@ -1,5 +1,5 @@
std = "selene_defs" std = "selene_defs"
exclude = ["**/node_modules/**"] exclude = ["*/node_modules/*"]
[lints] [lints]
global_usage = "allow" global_usage = "allow"