Add context()

This commit is contained in:
aaron 2024-10-06 15:20:04 +01:00
parent 83db00a073
commit 8142acd1c1
8 changed files with 303 additions and 1 deletions

View file

@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added
- `context()`
### Fixed ### Fixed
- Error stack traces being lost. - Error stack traces being lost.

View file

@ -34,7 +34,11 @@ Creates a new source with the given value.
- **Type** - **Type**
```lua ```lua
function source<T>(value: T): (T?) -> T function source<T>(value: T): Source<T>
type Source<T> =
() -> T -- get
& (T) -> () -- set
``` ```
- **Details** - **Details**

View file

@ -90,4 +90,45 @@ trigger effects until after the function finishes running.
only cause the effect to run once after the batch call ends instead of after only cause the effect to run once after the batch call ends instead of after
each time a source is updated. each time a source is updated.
## context()
Creates a new context.
- **Type**
```lua
function context<T>(default: T): Context<T>
type Context<T> =
() -> T -- get
& (T, () -> ()) -> () -- set
```
- **Details**
Calling `context()` returns a new context function.
Call this function with no arguments to get the context value.
Call this function with a value and a callback to set a new context with the
given value.
- **Example**
```lua
local theme = context()
local function Button()
print(theme())
end
root(function()
theme("light", function()
Button() -- prints "light"
theme("dark", function()
Button() -- prints "dark"
end)
end)
end)
```
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

75
src/context.luau Normal file
View file

@ -0,0 +1,75 @@
if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
local create_node = graph.create_node
local get_scope = graph.get_scope
local push_scope = graph.push_scope
local pop_scope = graph.pop_scope
local set_context = graph.set_context
export type Context<T> = (() -> T) & ((T, () -> ()) -> ())
local nil_symbol = newproxy()
local count = 0
local function context<T>(...: T): Context<T>
count += 1
local id = count
local has_default = select("#", ...) > 0
local default_value = ...
return function(...)
local scope: Node<unknown>? | false = get_scope()
if select("#", ...) == 0 then -- get
while scope do
local ctx = scope.context
if not ctx then
scope = scope.owner
continue
end
local value = (ctx :: { unknown })[id]
if value == nil then
scope = scope.owner
continue
end
return (if value ~= nil_symbol then value else nil) :: T
end
if has_default ~= nil then
return default_value
else
throw("attempt to get context when no context is set and no default context is set")
end
else -- set
if not scope then return throw("attempt to set context outside of a vide scope") end
local value, component = ...
local new_scope = create_node(scope, false, false)
set_context(new_scope, id, if value == nil then nil_symbol else value)
push_scope(new_scope)
local function efn(err: string) return debug.traceback(err, 3) end
local ok, result = xpcall(component, efn)
pop_scope()
if not ok then
throw(`error while running context:\n\n{result}`)
end
end
return nil :: any
end
end
return context

View file

@ -13,6 +13,8 @@ export type Node<T> = {
effect: ((T) -> T) | false, effect: ((T) -> T) | false,
cleanups: { () -> () } | false, cleanups: { () -> () } | false,
context: { [number]: unknown } | false,
owned: { Node<T> } | false, owned: { Node<T> } | false,
owner: Node<T> | false, owner: Node<T> | false,
@ -241,6 +243,8 @@ local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T
effect = effect, effect = effect,
cleanups = false, cleanups = false,
context = false,
owner = owner, owner = owner,
owned = false, owned = false,
@ -266,6 +270,14 @@ local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> } return { unpack(node) } :: { Node<any> }
end end
local function set_context<T>(node: Node<T>, key: number, value: unknown)
if node.context then
node.context[key] = value
else
node.context = { [key] = value }
end
end
return table.freeze { return table.freeze {
push_scope = push_scope, push_scope = push_scope,
pop_scope = pop_scope, pop_scope = pop_scope,
@ -283,5 +295,6 @@ return table.freeze {
get_children = get_children, get_children = get_children,
flush_update_queue = flush_update_queue, flush_update_queue = flush_update_queue,
get_update_queue_length = get_update_queue_length, get_update_queue_length = get_update_queue_length,
set_context = set_context,
scopes = scopes scopes = scopes
} }

View file

@ -16,6 +16,7 @@ local cleanup = require(script.cleanup)
local untrack = require(script.untrack) local untrack = require(script.untrack)
local read = require(script.read) local read = require(script.read)
local batch = require(script.batch) local batch = require(script.batch)
local context = require(script.context)
local switch = require(script.switch) local switch = require(script.switch)
local show = require(script.show) local show = require(script.show)
local indexes, values = require(script.maps)() local indexes, values = require(script.maps)()
@ -26,6 +27,9 @@ local throw = require(script.throw)
local flags = require(script.flags) local flags = require(script.flags)
export type Source<T> = source.Source<T> export type Source<T> = source.Source<T>
export type source<T> = Source<T>
export type Context<T> = context.Context<T>
export type context<T> = Context<T>
local function step(dt: number) local function step(dt: number)
if game then if game then
@ -63,6 +67,7 @@ local vide = {
untrack = untrack, untrack = untrack,
read = read, read = read,
batch = batch, batch = batch,
context = context,
-- animations -- animations
spring = spring, spring = spring,

View file

@ -4,11 +4,14 @@ local BENCH, START = testkit.benchmark()
local vide = require "src/init" local vide = require "src/init"
local source = vide.source local source = vide.source
local derive = vide.derive local derive = vide.derive
local effect = vide.effect
local indexes = vide.indexes local indexes = vide.indexes
local values = vide.values local values = vide.values
local batch = vide.batch local batch = vide.batch
local cleanup = vide.cleanup local cleanup = vide.cleanup
local untrack = vide.untrack
local create = vide.create local create = vide.create
local context = vide.context
assert(not vide.strict) assert(not vide.strict)
@ -446,6 +449,61 @@ ROOT_BENCH("values() all remove", function()
src(data) src(data)
end) end)
TITLE "context()"
ROOT_BENCH("set context", function()
local ctx = context()
for i = 1, START(N) do
ctx(i, function() end)
end
end)
ROOT_BENCH("get context (depth=1)", function()
local ctx = context()
local function run()
for i = 1, START(N) do
ctx()
end
end
ctx(1, function()
run()
end)
end)
local depth = 10
ROOT_BENCH(`get context (depth={depth})`, function()
local ctx = context()
local function run()
for i = 1, START(N) do
ctx()
end
end
local function nest_effect(fn)
untrack(function()
effect(fn)
return nil
end)
end
local f = run
for i = 1, depth - 1 do
local f_inner = f
f = function()
nest_effect(f_inner)
end
end
ctx(1, function()
f()
end)
end)
N *= 1024 N *= 1024
TITLE "aggregate" TITLE "aggregate"

View file

@ -2155,6 +2155,108 @@ TEST("read()", wrap_root(function()
end end
end)) end))
TEST("context()", function()
local root = vide.root
local context = vide.context
local effect = vide.effect
local untrack = vide.untrack
local show = vide.show
do CASE "set context"
local ctx = context()
root(function()
ctx(1, function()
CHECK(ctx() == 1)
effect(function()
CHECK(ctx() == 1)
end)
end)
end)
end
do CASE "set context outside of scope"
local ctx = context()
local ok = pcall(function()
ctx(1, function() end)
end)
CHECK(not ok)
end
do CASE "get default context"
local ctx = context(1)
CHECK(ctx() == 1)
root(function()
ctx(2, function()
CHECK(ctx() == 2)
end)
CHECK(ctx() == 1)
end)
end
do CASE "context cascade"
local ctx = context(1)
local ctx2 = context()
root(function()
ctx(2, function()
ctx2(true, function()
show(function() return true end, function()
ctx(3, function()
effect(function()
CHECK(ctx() == 3)
untrack(function()
effect(function()
CHECK(ctx() == 3)
end)
CHECK(ctx2() == true)
return {}
end)
end)
CHECK(ctx() == 3)
end)
CHECK(ctx() == 2)
return {}
end)
end)
CHECK(ctx() == 2)
end)
CHECK(ctx() == 1)
end)
end
do CASE "nil context"
local ctx = context(nil)
root(function()
CHECK(ctx() == nil)
ctx(nil, function()
CHECK(ctx() == nil)
ctx(true :: any, function()
CHECK(ctx() == true)
ctx(nil, function()
effect(function()
untrack(function()
effect(function()
CHECK(ctx() == nil)
end)
return {}
end)
end)
end)
end)
end)
end)
end
end)
TEST("nested effects cases", function() TEST("nested effects cases", function()
local vide = require "src/init" local vide = require "src/init"
local source = vide.source local source = vide.source