diff --git a/src/indexes.luau b/src/indexes.luau index acad4c0..d13d1c9 100644 --- a/src/indexes.luau +++ b/src/indexes.luau @@ -1,114 +1,118 @@ local flags = require "./flags" -local graph = require "./graph" -type Node = graph.Node -type SourceNode = graph.SourceNode -local create_node = graph.create_node -local create_source_node = graph.create_source_node -local push_scope_as_child_of = graph.push_scope_as_child_of -local update_descendants = graph.update_descendants -local assert_stable_scope = graph.assert_stable_scope -local push_scope = graph.push_scope -local pop_scope = graph.pop_scope -local evaluate_node = graph.evaluate_node -local destroy = graph.destroy +local branch = require "./branch" +local source = require "./source" +local effect = require "./effect" +local timeout = require "./timeout" () +type Array = { T } type Map = { [K]: V } +type Source = () -> T -local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO, delay: number?): () -> { VO } - local owner = assert_stable_scope() - local subowner = create_node(owner, false, false) +local function indexes( + input: Source>, + component: (Source, K, Source) -> (Obj, number?) +): Source> + local update_count = 0 + local caches = {} :: Map (), + present: (boolean?) -> boolean, + value: V?, + value_source: (V?) -> V, + object: Obj, + delay: number, + timeout: { cancel: boolean }?, + count: number, + }> - local input_cache = {} :: Map - local output_cache = {} :: Map - local input_nodes = {} :: Map> - local scopes = {} :: Map> - - local function update_children(data) - local children_need_update = false - - -- remove old indexes - for i in input_cache do - if data[i] == nil then - destroy(scopes[i]) - input_cache[i] = nil - output_cache[i] = nil - input_nodes[i] = nil - scopes[i] = nil - children_need_update = true - end + local output = source({} :: Array) + local function update_output() + local array = table.create(#caches) + for _, cache in caches do + table.insert(array, cache.object) end + output(array) + end - push_scope(subowner) + effect(function() + local data = input() - -- process new or changed values + local count = update_count + update_count += 1 + + local children_need_update = false -- set to true if a scope is created or destroyed + + -- process data for i, v in data do - local cv = input_cache[i] + local cache = caches[i] - if cv ~= v then - input_cache[i] = v + if cache == nil then -- create new scope and create component + local value_source = source(v) + local present = source(false) - if cv == nil then -- create new scope and run transform - local scope = create_node(subowner, false, false) - scopes[i] = scope :: Node + local delay = nil :: number? + local destroy, object = branch(function() + local object, t = component(value_source, i, present) + delay = t + return object + end) - local node = create_source_node(v) + present(true) + + children_need_update = true - push_scope(scope) + caches[i] = { + count = count, + value = v, + destroy_scope = destroy, + value_source = value_source, + present = present, + delay = delay or 0, + object = object + } + else -- update source + cache.count = count - local ok, result = xpcall(transform, debug.traceback, function() - push_scope_as_child_of(node) - return node.cache - end, i) - - pop_scope() - - if not ok then - pop_scope() -- subowner scope - error(result, 0) + if cache.value ~= v then + if cache.timeout then + cache.timeout.cancel = true + cache.timeout = nil + cache.present(true) end - input_nodes[i] = node - output_cache[i] = result - children_need_update = true - else -- update source - input_nodes[i].cache = v - update_descendants(input_nodes[i]) + cache.value = v + cache.value_source(v) end end end - pop_scope() + -- remove old indexes + for i, cache in caches do + if cache.count < count then -- if count is not latest then value is no longer in the input table + cache.present(false) + + if cache.delay == 0 then + cache.destroy_scope() + caches[i] = nil + children_need_update = true + else + cache.value = nil + if cache.timeout == nil then + cache.timeout = timeout(cache.delay, function() -- todo: avoid redundant updates (e.g. indexes() input is cleared) + cache.destroy_scope() + caches[i] = nil + update_output() + end) + end + end + end + end if children_need_update then - local output_array_size = #output_cache - local output_array - - -- check if the table contains a dictionary section - if output_array_size > 0 and next(output_cache, output_array_size) == nil then - output_array = table.clone(output_cache) - else - output_array = table.create(output_array_size) - for _, v in output_cache do - table.insert(output_array, v) - end - end - - return output_array - else - return nil + update_output() end - end + end) - local node = create_node(owner, function(pre_children) - return update_children(input()) or pre_children - end, {}) - - evaluate_node(node) - - return function() - push_scope_as_child_of(node) - return node.cache - end + return output end return indexes diff --git a/src/switch.luau b/src/switch.luau index b770d26..38a7586 100644 --- a/src/switch.luau +++ b/src/switch.luau @@ -8,7 +8,10 @@ type Map = { [K]: V } type Source = () -> T type Component = (Source) -> T -local function switch_map(input: Source, map: Map>): Source> +local function switch_map( + input: Source, + map: Map> +): Source> local output = source(nil :: nil | Obj | Array) local caches = {} :: Map = graph.Node -type SourceNode = graph.SourceNode -local create_node = graph.create_node -local create_source_node = graph.create_source_node -local push_scope_as_child_of = graph.push_scope_as_child_of -local update_descendants = graph.update_descendants -local assert_stable_scope = graph.assert_stable_scope -local push_scope = graph.push_scope -local pop_scope = graph.pop_scope -local evaluate_node = graph.evaluate_node -local destroy = graph.destroy +local branch = require "./branch" +local source = require "./source" +local effect = require "./effect" +local timeout = require "./timeout" () +type Array = { T } type Map = { [K]: V } +type Source = () -> T -local function check_primitives(t: {}) - if not flags.strict then return end - - for _, v in t do - if type(v) == "table" or type(v) == "userdata" or type(v) == "function" then continue end - error("table source map cannot return primitives", 0) - end -end - -local function values(input: () -> Map, transform: (VI, () -> K) -> VO, delay: number?): () -> { VO } - local owner = assert_stable_scope() - local subowner = create_node(owner, false, false) - +local function values( + input: Source>, + component: (V, Source, Source) -> (Obj, number?) +): Source> local update_count = 0 - - local caches = {} :: Map (), + present: (boolean?) -> boolean, + index: K?, + index_source: (K?) -> K, + object: Obj, + delay: number, + timeout: { cancel: boolean }?, count: number, - index: K, - scope: Node, - index_source: SourceNode, - alive_source: SourceNode, - result: VO, }> - local function update_children(data: Map) + local output = source({} :: Array) + local function update_output() + local array = table.create(16) + for _, cache in caches do + table.insert(array, cache.object) + end + output(array) + end + + effect(function() + local data = input() + local count = update_count update_count += 1 - local children_need_update = false + local children_need_update = false -- set to true if a scope is created or destroyed if flags.strict then - local cache = {} + local map = {} for _, v in data do - if cache[v] ~= nil then - error "duplicate table value detected" + if map[v] then + error("table source passed to `values()` contains duplicate values", 0) end - cache[v] = true + map[v] = true end end - push_scope(subowner) - -- process data for i, v in data do local cache = caches[v] - if cache == nil then -- create new scope and run transform - local scope = create_node(subowner, false, false) - local index_source = create_source_node(i) + if cache == nil then -- create new scope and create component + local index_source = source(i) + local present = source(false) - local new_cache = { + local delay = nil :: number? + local destroy, object = branch(function() + local object, t = component(v, index_source, present) + delay = t + return object + end) + + present(true) + + children_need_update = true + + caches[v] = { count = count, index = i, - scope = scope :: Node, - index_source = index_source :: SourceNode, - alive_source = nil :: any, - result = false :: any, + destroy_scope = destroy, + index_source = index_source, + present = present, + delay = delay or 0, + object = object } - -- must be set before transform is run so that the scope can - -- be destroyed if the transform itself updates the input - -- which lets the strict active scope destruction check work - caches[v] = new_cache - - push_scope(scope) - - local ok, result = xpcall(transform, debug.traceback, v, function() - push_scope_as_child_of(index_source) - return index_source.cache - end) - - pop_scope() - - if not ok then - pop_scope() -- subowner scope - error(result, 0) - end - - new_cache.result = result - children_need_update = true else -- update source cache.count = count if cache.index ~= i then + if cache.timeout then + cache.timeout.cancel = true + cache.timeout = nil + cache.present(true) + end + cache.index = i - cache.index_source.cache = i - update_descendants(cache.index_source) + cache.index_source(i) end end end - pop_scope() - -- remove old values for v, cache in caches do - if cache.count < count then - destroy(cache.scope) - caches[v] = nil - children_need_update = true + if cache.count < count then -- if count is not latest then value is no longer in the input table + cache.present(false) + + if cache.delay == 0 then + cache.destroy_scope() + caches[v] = nil + children_need_update = true + else + cache.index = nil + if cache.timeout == nil then + cache.timeout = timeout(cache.delay, function() -- todo: avoid redundant updates (e.g. values() input is cleared) + cache.destroy_scope() + caches[v] = nil + update_output() + end) + end + end end end if children_need_update then - local output_array = table.create(#data) - for _, cache in caches do - table.insert(output_array, cache.result) - end - check_primitives(output_array) - - return output_array - else - return nil + update_output() end - end + end) - local node = create_node(owner, function(pre_children) -- todo: add test - return update_children(input()) or pre_children - end, {}) - - evaluate_node(node) - - return function() - push_scope_as_child_of(node) - return node.cache - end + return output end return values diff --git a/test/benchmarks.luau b/test/benchmarks.luau index 690c8b9..cbb55f9 100644 --- a/test/benchmarks.luau +++ b/test/benchmarks.luau @@ -27,7 +27,7 @@ local function ROOT_BENCH(name: string, fn: () -> ()) end)() end -local N = 2^18 -- 262144 +local N = 2^20 TITLE "sources" diff --git a/test/tests.luau b/test/tests.luau index 7b068a6..1df61e9 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1625,9 +1625,12 @@ TEST("indexes()", wrap_root(function() do -- check that `input` allows gc of `output` local input = source {} - local output = indexes(input, function(v, i) - return v, i + local destroy, output = root(function() + return indexes(input, function(v, i) + return v, i + end) end) + destroy() local wref = weak { output } @@ -1731,6 +1734,81 @@ TEST("indexes()", wrap_root(function() vide.strict = false end + + do CASE "delay" + local input = source {} + + local cleaned_counts = {} :: Map + + local output = indexes(input, function(v, i, present) + cleanup(function() + cleaned_counts[i] = (cleaned_counts[i] or 0) + 1 + end) + return { value = v, index = i, present = present }, 1 + end) + + local function mapped() + local map = {} + local objects = output() + if objects then + for _, object in objects do + map[object.index] = { value = object.value, present = object.present } + end + end + return map + end + + ------------------------------------------------------------------------ + + do + CHECK(mapped()[1] == nil) + end + + input { 1, 2 } + + do + CHECK(mapped()[1].value() == 1) + CHECK(mapped()[1].present()) + + CHECK(mapped()[2].value() == 2) + CHECK(mapped()[2].present()) + end + + input { 2 } + step(0.5) + + do + CHECK(mapped()[1].value() == 2) + CHECK(mapped()[1].present()) + + CHECK(mapped()[2].value() == 2) + CHECK(not mapped()[2].present()) + end + + input { 1, 2 } + step(0.5 + 0.01) + + do + CHECK(mapped()[1].value() == 1) + CHECK(mapped()[1].present()) + + CHECK(mapped()[2].value() == 2) + CHECK(mapped()[2].present()) + end + + input { 3 } + step(1 + 0.01) + + do + CHECK(mapped()[1].value() == 3) + CHECK(mapped()[1].present()) + + CHECK(not mapped()[2]) + + CHECK(cleaned_counts[1] == nil) + CHECK(cleaned_counts[2] == 1) + end + end end)) TEST("values()", wrap_root(function() @@ -1852,6 +1930,80 @@ TEST("values()", wrap_root(function() CHECK(n0 == n1) end + + do CASE "delay" + local input = source {} + + local cleaned_counts = {} :: Map + + local output = values(input, function(v, i, present) + cleanup(function() + cleaned_counts[v] = (cleaned_counts[v] or 0) + 1 + end) + return { value = v, index = i, present = present }, 1 + end) + + local function mapped() + local map = {} + local objects = output() + if objects then + for _, object in objects do + map[object.value] = { index = object.index, present = object.present } + end + end + return map + end + + ------------------------------------------------------------------------ + + do + CHECK(mapped()[1] == nil) + end + + input { 1 } + + do + CHECK(mapped()[1].index() == 1) + CHECK(mapped()[1].present()) + end + + input { 2 } + step(0.5) + + do + CHECK(mapped()[1].index() == 1) + CHECK(not mapped()[1].present()) + + CHECK(mapped()[2].index() == 1) + CHECK(mapped()[2].present()) + end + + input { 1, 2 } + step(0.5 + 0.01) + + do + CHECK(mapped()[1].index() == 1) + CHECK(mapped()[1].present()) + + CHECK(mapped()[2].index() == 2) + CHECK(mapped()[2].present()) + end + + input { 3 } + step(1 + 0.01) + + do + CHECK(not mapped()[1]) + CHECK(not mapped()[2]) + + CHECK(mapped()[3].index() == 1) + CHECK(mapped()[3].present()) + + CHECK(cleaned_counts[1] == 1) + CHECK(cleaned_counts[2] == 1) + CHECK(cleaned_counts[3] == nil) + end + end end)) TEST("spring()", wrap_root(function()