-
-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of contextuals (#299)
Initial implementation of contextuals
- Loading branch information
Showing
7 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
--!strict | ||
--!nolint LocalShadow | ||
|
||
--[[ | ||
Time-based contextual values, to allow for transparently passing values down | ||
the call stack. | ||
]] | ||
|
||
local Package = script.Parent.Parent | ||
local Types = require(Package.Types) | ||
-- Logging | ||
local logError = require(Package.Logging.logError) | ||
local parseError = require(Package.Logging.parseError) | ||
|
||
local class = {} | ||
|
||
local CLASS_METATABLE = {__index = class} | ||
local WEAK_KEYS_METATABLE = {__mode = "k"} | ||
|
||
--[[ | ||
Returns the current value of this contextual. | ||
]] | ||
function class:now(): unknown | ||
local thread = coroutine.running() | ||
local value = self._valuesNow[thread] | ||
if typeof(value) ~= "table" then | ||
return self._defaultValue | ||
else | ||
local value: {value: unknown} = value :: any | ||
return value.value | ||
end | ||
end | ||
|
||
--[[ | ||
Temporarily assigns a value to this contextual. | ||
]] | ||
function class:is( | ||
newValue: unknown | ||
) | ||
local methods = {} | ||
-- Methods use colon `:` syntax for consistency and autocomplete but we | ||
-- actually want them to operate on the `self` from this outer lexical scope | ||
local contextual = self | ||
|
||
function methods:during<T, A...>( | ||
callback: (A...) -> T, | ||
...: A... | ||
): T | ||
local thread = coroutine.running() | ||
local prevValue = contextual._valuesNow[thread] | ||
-- Storing the value in this format allows us to distinguish storing | ||
-- `nil` from not calling `:during()` at all. | ||
contextual._valuesNow[thread] = { value = newValue } | ||
local ok, value = xpcall(callback, parseError, ...) | ||
contextual._valuesNow[thread] = prevValue | ||
if ok then | ||
return value | ||
else | ||
logError("contextualCallbackError", value) | ||
end | ||
end | ||
|
||
return methods | ||
end | ||
|
||
local function Contextual<T>( | ||
defaultValue: T | ||
): Types.Contextual<T> | ||
local self = setmetatable({ | ||
type = "Contextual", | ||
-- if we held strong references to threads here, then if a thread was | ||
-- killed before this contextual had a chance to finish executing its | ||
-- callback, it would be held strongly in this table forever | ||
_valuesNow = setmetatable({}, WEAK_KEYS_METATABLE), | ||
_defaultValue = defaultValue | ||
}, CLASS_METATABLE) | ||
|
||
return self | ||
end | ||
|
||
return Contextual |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
local Package = game:GetService("ReplicatedStorage").Fusion | ||
local Contextual = require(Package.Utility.Contextual) | ||
|
||
return function() | ||
it("should construct a Contextual object", function() | ||
local ctx = Contextual() | ||
|
||
expect(ctx).to.be.a("table") | ||
expect(ctx.type).to.equal("Contextual") | ||
end) | ||
|
||
it("should provide its default value", function() | ||
local ctx = Contextual("foo") | ||
|
||
expect(ctx:now()).to.equal("foo") | ||
end) | ||
|
||
it("should correctly scope temporary values", function() | ||
local ctx = Contextual("foo") | ||
|
||
expect(ctx:now()).to.equal("foo") | ||
|
||
ctx:is("bar"):during(function() | ||
expect(ctx:now()).to.equal("bar") | ||
|
||
ctx:is("baz"):during(function() | ||
expect(ctx:now()).to.equal("baz") | ||
end) | ||
|
||
expect(ctx:now()).to.equal("bar") | ||
end) | ||
|
||
expect(ctx:now()).to.equal("foo") | ||
end) | ||
|
||
it("should allow for argument passing", function() | ||
local ctx = Contextual("foo") | ||
|
||
local function test(a, b, c, d) | ||
expect(a).to.equal("a") | ||
expect(b).to.equal("b") | ||
expect(c).to.equal("c") | ||
expect(d).to.equal("d") | ||
end | ||
|
||
ctx:is("bar"):during(test, "a", "b", "c", "d") | ||
end) | ||
|
||
it("should not interfere across coroutines", function() | ||
local ctx = Contextual("foo") | ||
|
||
local coro1 = coroutine.create(function() | ||
ctx:is("bar"):during(function() | ||
expect(ctx:now()).to.equal("bar") | ||
coroutine.yield() | ||
expect(ctx:now()).to.equal("bar") | ||
end) | ||
end) | ||
|
||
local coro2 = coroutine.create(function() | ||
ctx:is("baz"):during(function() | ||
expect(ctx:now()).to.equal("baz") | ||
coroutine.yield() | ||
expect(ctx:now()).to.equal("baz") | ||
end) | ||
end) | ||
|
||
coroutine.resume(coro1) | ||
expect(ctx:now()).to.equal("foo") | ||
coroutine.resume(coro2) | ||
expect(ctx:now()).to.equal("foo") | ||
coroutine.resume(coro1) | ||
expect(ctx:now()).to.equal("foo") | ||
coroutine.resume(coro2) | ||
expect(ctx:now()).to.equal("foo") | ||
end) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters