--!strict --!nolint LocalShadow --[[ Time-based contextual values, to allow for transparently passing values down the call stack. ]] local Types = require "../Types" -- Logging local logError = require "../Logging/logError" local parseError = require "../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 return value.value :: unknown 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(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, ...) local ok, value = pcall(callback, ...) contextual._valuesNow[thread] = prevValue if ok then return value else logError("contextualCallbackError", parseError(value)) end end return methods end local function Contextual(defaultValue: T): Types.Contextual 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