This commit is contained in:
aaron 2023-07-28 13:08:48 +01:00
parent 9021d04e1e
commit 1aa74310f3
6 changed files with 102 additions and 47 deletions

View file

@ -39,7 +39,7 @@ local getChangeSymbol = memoize(function(name: string): Types.Symbol<MaybeState<
end
end)
state.updated = true
state.__updated = true
bind.event(state :: State<any>, instance, event)
else
error("Attempt to connect non-function to changed event", 2)

View file

@ -24,7 +24,7 @@ local function derive<T>(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): St
end
local value: T = captureAndLink(node, deriveValue)
node.cache = value
rawset(node, "__cache", value)
return node :: State<T>
end

View file

@ -8,11 +8,11 @@ local flags = require(script.Parent.flags)
export type State<T> = typeof(setmetatable(
{} :: {
cache: T,
updated: boolean,
derive: (any) -> T,
effects: { [(unknown) -> ()]: unknown } | false, -- weak values
children: { State<T> } | false -- weak values
__cache: T,
__updated: boolean,
__derive: (any) -> T,
__effects: { [(unknown) -> ()]: unknown } | false, -- weak values
__children: { State<T> } | false -- weak values
}, {} :: {
__concat: (any, any) -> any,
__add: (any, any) -> any,
@ -32,7 +32,7 @@ export type MaybeState<T> = State<T> | T
export type Unwrapper = <T>(T) -> T
local WEAK_VALUES_RESIZABLE = { __mode = "vs" }
local EVALUATION_ERR = "Error while evaluating state:\n\n"
local EVALUATION_ERR = "error while evaluating state:\n\n"
local State = {}
@ -56,7 +56,7 @@ local checkForYield do
if not ok then
if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then
error(EVALUATION_ERR .. "Cannot yield when deriving state in watcher", 3)
error(EVALUATION_ERR .. "cannot yield when deriving state in watcher", 3)
else
error(EVALUATION_ERR..err, 3)
end
@ -65,16 +65,16 @@ local checkForYield do
end
local function setEffect<T>(state: State<unknown>, fn: (T) -> (), key: T)
if not state.effects then
state.effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any
if not state.__effects then
state.__effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any
else
state.effects[fn :: () -> ()] = key
state.__effects[fn :: () -> ()] = key
end
end
local function runEffects(state: State<unknown>)
if state.effects then
for effect, key in next, state.effects do
if state.__effects then
for effect, key in next, state.__effects do
if flags.strict then effect(key) end
effect(key)
end
@ -84,17 +84,17 @@ end
-- retrieves a state's cached value
-- recalculates value if an ancestor was updated
local function get<T>(state: State<T>): T
if state.updated then
state.updated = false
if state.__updated then
state.__updated = false
if flags.strict then checkForYield(state.derive) end
if flags.strict then checkForYield(state.__derive) end
local ok, result: T|string? = pcall(state.derive, unwrap); if ok then
rawset(state :: any, "cache", result :: T)
else error(EVALUATION_ERR .. result :: string, 2) end
local ok, result: T|string? = pcall(state.__derive, unwrap); if ok then
rawset(state :: any, "__cache", result :: T)
else error(EVALUATION_ERR .. result :: string, 0) end
end
return state.cache
return rawget(state :: any, "__cache")
end
-- utility function for retrieving a value from state and allowing passthrough of non-state
@ -107,20 +107,20 @@ unwrap = function<T>(value: MaybeState<T>): T
end
local function addChild(parent: State<unknown>, child: State<unknown>)
if parent.children then
table.insert(parent.children, child)
if parent.__children then
table.insert(parent.__children, child)
else
parent.children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any
parent.__children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any
end
end
-- marks all state descendants for recalculation and runs effects
local function update(state: State<unknown>)
runEffects(state)
if state.children then
for _, child in state.children do
if not child.updated then
child.updated = true
if state.__children then
for _, child in state.__children do
if not child.__updated then
child.__updated = true
update(child)
end
end
@ -129,13 +129,13 @@ end
-- sets a state's cached value and updates all descendants
local function set<T>(state: State<T>, value: T)
state.cache = value
state.__cache = value
update(state)
end
-- links two states as parent-child
local function link(parent: State<unknown>, child: State<unknown>, derive: () -> unknown)
child.derive = derive
child.__derive = derive
addChild(parent, child)
end
@ -154,7 +154,7 @@ local function capture<T>(callback: (Unwrapper) -> T): ({ State<unknown> }, T)
end
end)
if not ok then error("Error while detecting watcher: " .. result :: string, 2) end
if not ok then error("error while detecting watcher: " .. result :: string, 0) end
return states, result :: T
end
@ -163,7 +163,7 @@ end
local function captureAndLink<T>(child: State<T>, callback: (Unwrapper) -> T): T
local states, value = capture(callback)
child.derive = callback
child.__derive = callback
for _, parent: State<unknown> in next, states do
addChild(parent, child)
end
@ -191,30 +191,61 @@ local function overload(op: (unknown, unknown) -> unknown): (any, any) -> any
link(b :: State<unknown>, derived, function() return op(a, get(b :: State<unknown>)) end)
end
derived.updated = true
derived.__updated = true
return derived
end
end
function State.__index(_, index)
if index == "cache" then return nil end -- todo: better solution
error("attempt to index state", 2)
local function __unm(self: State<unknown>)
local derived = create(nil :: any)
link(self, derived, function()
return -get(self) :: number
end)
derived.__updated = true
return derived
end
local function __index(self: State<unknown>, index: unknown)
local derived = create(nil :: any)
link(self, derived, function()
return (get(self) :: {})[index]
end)
derived.__updated = true
return derived
end
State.__index = __index
State.__concat = overload(function(a: any, b: any) return tostring(a) .. tostring(b) end)
State.__add = overload(function(a: any, b: any) return a + b end)
State.__sub = overload(function(a: any, b: any) return a - b end)
State.__mul = overload(function(a: any, b: any) return a * b end)
State.__div = overload(function(a: any, b: any) return a / b end)
--State.__eq = overload(function(a: any, b: any) return a == b end)
State.__pow = overload(function(a: any, b: any) return a ^ b end)
State.__mod = overload(function(a: any, b: any) return a % b end)
State.__unm = __unm
-- todo: what to do
do
local function err()
error("cannot perform equality comparison with state", 2)
end
State.__eq = err
State.__lt = err
State.__le = err
end
function create<T>(value: T): State<T>
return setmetatable({
cache = value,
updated = false,
derive = function() return nil end :: any,
effects = false :: false,
children = false :: false
__cache = value,
__updated = false,
__derive = function() return nil end :: any,
__effects = false :: false,
__children = false :: false
}, State)
end

View file

@ -55,7 +55,7 @@ local function map<K, VI, VO>(input: unknown, transform: (K, VI) -> VO, cleanup:
end
link(input :: State<Map<K, VI>>, output, derive)
output.updated = true
output.__updated = true
return output
elseif type(input) == "table" then

View file

@ -27,7 +27,7 @@ local function wrap<T>(value: MaybeState<T>?): (State<T>, Setter<T>)
local v = if wrapped(vi) then get(vi :: State<T>) else vi :: T
if v ~= state.cache or force then
if v ~= state.__cache or force then
set(state, v)
elseif flags.strict and type(v) == "table" then
throw("attempt to set same table object")

View file

@ -105,8 +105,8 @@ TEST("graph", function()
set(a, get(a) + 1) -- mark `b` for recomputation again
captureAndLink(c, function(from) return from(b) end)
-- check that only `b` was linked
CHECK(not rawfind(assert(a.children), c))
CHECK(rawfind(assert(b.children), c))
CHECK(not rawfind(assert(a.__children), c))
CHECK(rawfind(assert(b.__children), c))
end
do CASE "Nodes garbage collection"
@ -291,6 +291,30 @@ TEST("derive()", function()
set(2)
CHECK(unwrap(text) == "22")
end
do
local a, seta = wrap { b = { c = 1 } }
local b = a.b
local c = b.c
CHECK(unwrap(c) == 1)
seta { b = { c = 2 } }
CHECK(unwrap(c) == 2)
end
do
local state, set = wrap { profiles = { decimal = { level = 1 }}}
local stringified = "Level: " .. state.profiles.decimal.level
set(function(state)
state.profiles.decimal.level += 1
return state
end, true)
CHECK(unwrap(stringified) == "Level: 2")
end
end
do CASE "Derive from updated"
@ -704,7 +728,7 @@ TEST("create()", function()
}
wref.instance = instance
wref.binding = next((state :: any).effects)
wref.binding = next((state :: any).__effects)
end
CHECK(wref.binding)