Skip to content

Commit

Permalink
Add context()
Browse files Browse the repository at this point in the history
  • Loading branch information
centau committed Oct 6, 2024
1 parent 83db00a commit 8142acd
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased

### Added

- `context()`

### Fixed

- Error stack traces being lost.
Expand Down
6 changes: 5 additions & 1 deletion docs/api/reactivity-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ Creates a new source with the given value.
- **Type**

```lua
function source<T>(value: T): (T?) -> T
function source<T>(value: T): Source<T>

type Source<T> =
() -> T -- get
& (T) -> () -- set
```

- **Details**
Expand Down
41 changes: 41 additions & 0 deletions docs/api/reactivity-utility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 changes: 75 additions & 0 deletions src/context.luau
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions src/graph.luau
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type Node<T> = {
effect: ((T) -> T) | false,
cleanups: { () -> () } | false,

context: { [number]: unknown } | false,

owned: { Node<T> } | false,
owner: Node<T> | false,

Expand Down Expand Up @@ -241,6 +243,8 @@ local function create_node<T>(owner: false | Node<any>, effect: false | (T) -> T
effect = effect,
cleanups = false,

context = false,

owner = owner,
owned = false,

Expand All @@ -266,6 +270,14 @@ local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> }
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 {
push_scope = push_scope,
pop_scope = pop_scope,
Expand All @@ -283,5 +295,6 @@ return table.freeze {
get_children = get_children,
flush_update_queue = flush_update_queue,
get_update_queue_length = get_update_queue_length,
set_context = set_context,
scopes = scopes
}
5 changes: 5 additions & 0 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ local cleanup = require(script.cleanup)
local untrack = require(script.untrack)
local read = require(script.read)
local batch = require(script.batch)
local context = require(script.context)
local switch = require(script.switch)
local show = require(script.show)
local indexes, values = require(script.maps)()
Expand All @@ -26,6 +27,9 @@ local throw = require(script.throw)
local flags = require(script.flags)

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)
if game then
Expand Down Expand Up @@ -63,6 +67,7 @@ local vide = {
untrack = untrack,
read = read,
batch = batch,
context = context,

-- animations
spring = spring,
Expand Down
58 changes: 58 additions & 0 deletions test/benchmark.luau
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ local BENCH, START = testkit.benchmark()
local vide = require "src/init"
local source = vide.source
local derive = vide.derive
local effect = vide.effect
local indexes = vide.indexes
local values = vide.values
local batch = vide.batch
local cleanup = vide.cleanup
local untrack = vide.untrack
local create = vide.create
local context = vide.context

assert(not vide.strict)

Expand Down Expand Up @@ -446,6 +449,61 @@ ROOT_BENCH("values() all remove", function()
src(data)
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

TITLE "aggregate"
Expand Down
102 changes: 102 additions & 0 deletions test/tests.luau
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,108 @@ TEST("read()", wrap_root(function()
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()
local vide = require "src/init"
local source = vide.source
Expand Down

0 comments on commit 8142acd

Please sign in to comment.