mirror of
https://github.com/littensy/charm.git
synced 2026-10-21 11:44:08 +00:00
refactor: rewrite untracked, update alien-signals
This commit is contained in:
parent
a8051257e5
commit
cb52ece4d3
15 changed files with 239 additions and 114 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
10
rokit.toml
10
rokit.toml
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
std = "selene_defs"
|
std = "selene_defs"
|
||||||
exclude = ["**/node_modules/**"]
|
exclude = ["*/node_modules/*"]
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
global_usage = "allow"
|
global_usage = "allow"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue