refactor: align with alien-signals 3.0

This commit is contained in:
littensy 2025-10-18 21:59:34 -07:00
parent 7a63993293
commit bf92413a97
5 changed files with 254 additions and 279 deletions

View file

@ -5,7 +5,7 @@ local computed = core.computed
local effect = core.effect
local effectScope = core.effectScope
local endBatch = core.endBatch
local setCurrentSub = core.setCurrentSub
local setActiveSub = core.setActiveSub
local onCleanup = core.onCleanup
local atom = core.atom
local startBatch = core.startBatch
@ -202,9 +202,9 @@ test("should duplicate subscribers do not affect the notify order", function()
effect(function()
table.insert(order, "a")
local currentSub = setCurrentSub(nil)
local currentSub = setActiveSub(nil)
local isOne = src2() == 1
setCurrentSub(currentSub)
setActiveSub(currentSub)
if isOne then
src1()
end

View file

@ -1,7 +1,3 @@
-- Adapted from Alien Signals
-- https://github.com/stackblitz/alien-signals
-- https://github.com/Nicell/alien-signals-luau
export type ReactiveNode = {
deps: Link?,
depsTail: Link?,
@ -34,7 +30,7 @@ local RECURSED_CHECK = 0b00000100
local RECURSED = 0b00001000
local DIRTY = 0b00010000
local PENDING = 0b00100000
local UNTRACKED = 0b10000000
local PEEKING = 0b10000000
local ReactiveFlags = {
None = NONE,
@ -44,7 +40,6 @@ local ReactiveFlags = {
Recursed = RECURSED,
Dirty = DIRTY,
Pending = PENDING,
Untracked = UNTRACKED,
}
local function createReactiveSystem(props: {
@ -55,7 +50,6 @@ local function createReactiveSystem(props: {
local update = props.update
local notify = props.notify
local unwatched = props.unwatched
local currentVersion = 0
local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean
local link = sub.depsTail
@ -68,23 +62,27 @@ local function createReactiveSystem(props: {
return false
end
local function link(dep: ReactiveNode, sub: ReactiveNode): ()
local function link(dep: ReactiveNode, sub: ReactiveNode, version: number): ()
-- DEVIATION: Do not link signals to sub while peeking
if bit32.btest(sub.flags, PEEKING) and bit32.btest(dep.flags, MUTABLE) then
return
end
local prevDep = sub.depsTail
if prevDep and prevDep.dep == dep then
return
end
local nextDep = if prevDep then prevDep.nextDep else sub.deps
if nextDep and nextDep.dep == dep then
nextDep.version = currentVersion
nextDep.version = version
sub.depsTail = nextDep
return
end
local prevSub = dep.subsTail
if prevSub and prevSub.version == currentVersion and prevSub.sub == sub then
if prevSub and prevSub.version == version and prevSub.sub == sub then
return
end
local newLink: Link = {
version = currentVersion,
version = version,
dep = dep,
sub = sub,
prevDep = prevDep,
@ -208,21 +206,6 @@ local function createReactiveSystem(props: {
until false
end
local function startTracking(sub: ReactiveNode): ()
currentVersion += 1
sub.depsTail = nil
sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK)
end
local function endTracking(sub: ReactiveNode): ()
local depsTail = sub.depsTail
local toRemove = if depsTail then depsTail.nextDep else sub.deps
while toRemove do
toRemove = unlink(toRemove, sub)
end
sub.flags = bit32.band(sub.flags, bit32.bnot(RECURSED_CHECK))
end
local function shallowPropagate(link: Link): ()
repeat
local sub = link.sub
@ -320,8 +303,6 @@ local function createReactiveSystem(props: {
unlink = unlink,
propagate = propagate,
checkDirty = checkDirty,
endTracking = endTracking,
startTracking = startTracking,
shallowPropagate = shallowPropagate,
}
end

View file

@ -1,7 +1,3 @@
-- Adapted from Alien Signals
-- https://github.com/stackblitz/alien-signals
-- https://github.com/Nicell/alien-signals-luau
local alien = require("./alien")
local config = require("./flags")
local utils = require("./utils")
@ -15,22 +11,23 @@ local createReactiveSystem = alien.createReactiveSystem
local deepFreeze = utils.deepFreeze
local noYield = utils.noYield
local NONE = 0b0000000
local MUTABLE = 0b0000001
local WATCHING = 0b0000010
local DIRTY = 0b0010000
local PENDING = 0b0100000
local QUEUED = 0b1000000
local UNTRACKED = 0b10000000
local NONE = 0b00000000
local MUTABLE = 0b00000001
local WATCHING = 0b00000010
local RECURSED_CHECK = 0b00000100
local DIRTY = 0b00010000
local PENDING = 0b00100000
local QUEUED = 0b01000000
local PEEKING = 0b10000000
type EffectScope = ReactiveNode & {
cleanups: { () -> () },
}
type Effect = ReactiveNode & {
fn: (dispose: () -> ()) -> ...(() -> ())?,
fn: (stop: () -> ()) -> (() -> ())?,
cleanups: { () -> () },
cleanup: () -> (),
stop: () -> (),
}
type Computed<T = any> = ReactiveNode & {
@ -38,54 +35,47 @@ type Computed<T = any> = ReactiveNode & {
getter: (previousValue: T?) -> T,
}
type Atom<T = any> = ReactiveNode & {
previousValue: T,
value: T,
equals: ((T, T) -> boolean)?,
type Signal<T = any> = ReactiveNode & {
currentValue: T,
pendingValue: T,
}
local link: (dep: ReactiveNode, sub: ReactiveNode) -> ()
local link: (dep: ReactiveNode, sub: ReactiveNode, version: number) -> ()
local unlink: (link: Link, sub: ReactiveNode?) -> Link?
local propagate: (link: Link) -> ()
local checkDirty: (link: Link, sub: ReactiveNode) -> boolean
local endTracking: (sub: ReactiveNode) -> ()
local startTracking: (sub: ReactiveNode) -> ()
local shallowPropagate: (link: Link) -> ()
local cycle = 0
local batchDepth = 0
local queuedEffects: { Effect } = {}
local queuedEffectsLength = 0
local notifyIndex = 0
local activeScope: Effect | EffectScope?
local queuedEffects: { Effect | EffectScope? } = {}
local queuedEffectsLength = 0
local activeSub: ReactiveNode?
local function getCurrentSub(): ReactiveNode?
local function getActiveSub(): ReactiveNode?
return activeSub
end
local function setCurrentSub(sub: ReactiveNode?)
local function setActiveSub(sub: ReactiveNode?)
local prevSub = activeSub
activeSub = sub
return prevSub
end
local function getCurrentScope(): Effect | EffectScope?
return activeScope
local function purgeDeps(sub: ReactiveNode)
local depsTail = sub.depsTail
local dep = if depsTail then depsTail.nextDep else sub.deps
while dep do
dep = unlink(dep, sub)
end
local function setCurrentScope(scope: Effect | EffectScope?)
local prevScope = activeScope
activeScope = scope
return prevScope
end
local function runCleanups(effect: EffectScope)
for _, cleanup in effect.cleanups do
local prev = setCurrentSub(nil)
local prevSub = setActiveSub(nil)
local success, result: any = (if config.strict then noYield else pcall)(cleanup)
setCurrentSub(prev)
activeSub = prevSub
if not success then
error(result)
end
@ -93,24 +83,32 @@ local function runCleanups(effect: EffectScope)
table.clear(effect.cleanups)
end
local function run(effect: Effect, flags: ReactiveFlags): ()
if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then
runCleanups(effect)
local prev = setCurrentSub(effect)
startTracking(effect)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(effect.fn, effect.cleanup)
setCurrentSub(prev)
endTracking(effect)
local function setNotPending(sub: ReactiveNode, flags: ReactiveFlags)
sub.flags = bit32.band(flags, bit32.bnot(PENDING))
end
local function run(e: Effect | EffectScope, flags: ReactiveFlags): ()
if
bit32.btest(flags, DIRTY)
or (bit32.btest(flags, PENDING) and (checkDirty(e.deps :: Link, e) or setNotPending(e, flags)))
then
runCleanups(e)
local e = e :: Effect
cycle += 1
e.depsTail = nil
e.flags = bit32.bor(WATCHING, RECURSED_CHECK)
local prevSub = setActiveSub(e)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(e.fn, e.stop)
activeSub = prevSub
e.flags = bit32.band(e.flags, bit32.bnot(RECURSED_CHECK))
purgeDeps(e)
if not success then
error(result)
elseif result then
table.insert(effect.cleanups, result)
table.insert(e.cleanups, result)
end
return
elseif bit32.btest(flags, PENDING) then
effect.flags = bit32.band(flags, bit32.bnot(PENDING))
end
local link = effect.deps
else
local link = e.deps
while link do
local dep = link.dep
local depFlags = dep.flags
@ -122,11 +120,12 @@ local function run(effect: Effect, flags: ReactiveFlags): ()
link = link.nextDep
end
end
end
local function flush(): ()
while notifyIndex < queuedEffectsLength do
notifyIndex += 1
local effect = queuedEffects[notifyIndex]
local effect = queuedEffects[notifyIndex] :: Effect
queuedEffects[notifyIndex] = nil
effect.flags = bit32.band(effect.flags, bit32.bnot(QUEUED))
run(effect, effect.flags)
@ -150,165 +149,136 @@ local function endBatch()
end
end
local function updateComputed(computed: Computed): boolean
local prevSub = setCurrentSub(computed)
startTracking(computed)
local oldValue = computed.value
local success, result = (if config.strict then noYield else pcall)(computed.getter, oldValue)
setCurrentSub(prevSub)
endTracking(computed)
local function updateComputed(c: Computed): boolean
cycle += 1
c.depsTail = nil
c.flags = bit32.bor(MUTABLE, RECURSED_CHECK)
local prevSub = setActiveSub(c)
local oldValue = c.value
local success, result: unknown = (if config.strict then noYield else pcall)(c.getter, oldValue)
activeSub = prevSub
c.flags = bit32.band(c.flags, bit32.bnot(RECURSED_CHECK))
purgeDeps(c)
if not success then
error(result)
end
if oldValue ~= result then
computed.value = result
if config.frozen then
deepFreeze(result)
end
c.value = result
return true
else
return false
end
end
local function updateAtom(atom: Atom, value: any): boolean
atom.flags = MUTABLE
if atom.previousValue ~= value then
atom.previousValue = value
local function updateSignal(s: Signal): boolean
s.flags = MUTABLE
local pendingValue = s.pendingValue
if s.currentValue ~= pendingValue then
s.currentValue = pendingValue
return true
else
return false
end
end
local function readAtom<T>(atom: Atom<T>): T
local value = atom.value
if bit32.btest(atom.flags, DIRTY) then
if updateAtom(atom, value) then
local subs = atom.subs
local function computedOper<T>(this: Computed<T>): () -> T
return function(): T
local flags = this.flags
if
bit32.btest(flags, DIRTY)
or (bit32.btest(flags, PENDING) and (checkDirty(this.deps :: Link, this) or setNotPending(this, flags)))
then
if updateComputed(this) then
local subs = this.subs
if subs then
shallowPropagate(subs)
end
end
end
if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then
link(atom, activeSub)
if activeSub then
link(this, activeSub, cycle)
end
return this.value :: T
end
return value
end
local function writeAtom<T>(atom: Atom<T>, oldValue: T, newValue: T): T
local equals = atom.equals
if if equals then not equals(oldValue, newValue) else oldValue ~= newValue then
if config.frozen then
deepFreeze(newValue)
local function signalOper<T>(this: Signal<T>, equals: ((current: T, incoming: T) -> boolean)?): (...((T) -> T) | T) -> T
return function(...: ((T) -> T) | T): T
if select("#", ...) ~= 0 then
local update = ...
local value: T
if type(update) == "function" then
local prevSub = setActiveSub(nil)
local success, result: T = (if config.strict then noYield else pcall)(update, this.currentValue)
activeSub = prevSub
if not success then
error(result)
end
atom.value = newValue
atom.flags = bit32.bor(MUTABLE, DIRTY)
local subs = atom.subs
value = result
else
value = update
end
if if equals then not equals(this.pendingValue, value) else this.pendingValue ~= value then
if config.frozen then
deepFreeze(value)
end
this.pendingValue = value
this.flags = bit32.bor(MUTABLE, DIRTY)
local subs = this.subs
if subs then
propagate(subs)
if batchDepth == 0 then
flush()
end
end
return newValue
return value
end
return oldValue
end
local function atomGetterSetter<T>(atom: Atom<T>): (...((T) -> T) | T) -> T
return function(...)
if select("#", ...) == 0 then
return readAtom(atom)
end
local oldValue = atom.value
local newValue = ...
if type(newValue) == "function" then
local prev = setCurrentSub(nil)
local success, result = (if config.strict then noYield else pcall)(newValue, oldValue)
setCurrentSub(prev)
if not success then
error(result)
end
newValue = result
end
return writeAtom(atom, oldValue, newValue :: T)
end
end
local function computedGetter<T>(computed: Computed<T>): () -> T
return function()
local flags = computed.flags
if
bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(computed.deps :: Link, computed))
then
if updateComputed(computed) then
local subs = computed.subs
return this.currentValue
else
if bit32.btest(this.flags, DIRTY) then
if updateSignal(this) then
local subs = this.subs
if subs then
shallowPropagate(subs)
end
end
elseif bit32.btest(flags, PENDING) then
computed.flags = bit32.band(flags, bit32.bnot(PENDING))
end
if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then
link(computed, activeSub)
elseif activeScope then
link(computed, activeScope)
local sub = activeSub
while sub do
if bit32.btest(sub.flags, bit32.bor(MUTABLE, WATCHING)) then
link(this, sub, cycle)
break
end
return computed.value :: T
end
end
local function effectStop(self: EffectScope): () -> ()
return function()
local dep = self.deps
while dep do
dep = unlink(dep, self)
end
local sub = self.subs
if sub then
unlink(sub)
end
self.flags = NONE
runCleanups(self)
end
end
local function update(atom: ReactiveNode): boolean
if (atom :: Computed).getter then
return updateComputed(atom :: Computed)
else
return updateAtom(atom :: Atom, (atom :: Atom).value)
end
end
local function notify(effect: ReactiveNode)
local flags = effect.flags
if not bit32.btest(flags, QUEUED) then
effect.flags = bit32.bor(flags, QUEUED)
local subs = effect.subs
local subs = sub.subs
if subs then
notify(subs.sub :: Effect)
else
queuedEffectsLength += 1
queuedEffects[queuedEffectsLength] = effect :: Effect
sub = subs.sub
end
end
return this.currentValue
end
end
end
local function unwatched(node: ReactiveNode)
if (node :: Computed).getter then
local toRemove = node.deps
if toRemove then
node.flags = bit32.bor(MUTABLE, DIRTY)
repeat
toRemove = unlink(toRemove, node)
until not toRemove
local function effectScopeOper(this: EffectScope): () -> ()
return function()
this.depsTail = nil
this.flags = NONE
purgeDeps(this)
local sub = this.subs
if sub then
unlink(sub, this)
end
elseif (node :: Effect).cleanups then
effectStop(node :: Effect)()
runCleanups(this)
end
end
local function effectOper(this: Effect): () -> ()
return effectScopeOper(this)
end
--- Creates a reactive atom that acts as both a getter and setter. Calling the
--- atom with an argument sets its value, while calling it with no arguments
--- returns its current value.
@ -316,16 +286,15 @@ end
--- @param value The initial value of the atom. If empty, the atom's value will be `undefined`.
--- @param equals A comparator function to determine if the atom's value has changed.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#atomstate-options
local function atom<T>(value: T, equals: ((T, T) -> boolean)?): (...((T) -> T) | T) -> T
local function atom<T>(initialValue: T, equals: ((current: T, incoming: T) -> boolean)?): (...((T) -> T) | T) -> T
if config.frozen then
deepFreeze(value)
deepFreeze(initialValue)
end
return atomGetterSetter({
previousValue = value,
value = value,
return signalOper({
currentValue = initialValue,
pendingValue = initialValue,
flags = MUTABLE,
equals = equals,
})
}, equals)
end
--- Creates a new atom that is computed based on the values of other atoms.
@ -345,11 +314,10 @@ end
--- @param getter A function that produces the next value.
--- @param value An optional initial value for the selector.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#computedcallback-options
local function computed<T>(getter: (previousValue: T?) -> T, value: T?): () -> T
return computedGetter({
local function computed<T>(getter: (previousValue: T?) -> T): () -> T
return computedOper({
flags = bit32.bor(MUTABLE, DIRTY),
getter = getter,
value = value,
})
end
@ -366,28 +334,32 @@ end
--- @param fn The function to run when dependencies change.
--- @return A function for disposing the effect.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectcallback
local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> ()
local self: Effect = {
local function effect(fn: (stop: () -> ()) -> ()): () -> ()
local e: Effect = {
fn = fn,
flags = WATCHING,
cleanups = {},
cleanup = nil :: any,
stop = nil :: any,
}
self.cleanup = effectStop(self)
if activeSub and config.trackInnerEffects then
link(self, activeSub)
elseif activeScope then
link(self, activeScope)
e.stop = effectOper(e)
local prevSub = setActiveSub(e)
if prevSub and (config.trackInnerEffects or prevSub.flags == NONE) then
link(e, prevSub, 0)
end
local prev = setCurrentSub(self)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn, self.cleanup)
setCurrentSub(prev)
local stopped = false
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn, function()
stopped = true
end)
activeSub = prevSub
if not success then
error(result)
elseif result then
table.insert(self.cleanups, result)
table.insert(e.cleanups, result)
end
return self.cleanup
if stopped then
e.stop()
end
return e.stop
end
--- Creates an effect scope that can capture multiple reactive effects (i.e.
@ -398,25 +370,23 @@ end
--- @param detached If true, the scope will not be automatically disposed of when the parent scope is disposed. Defaults to false.
--- @return A function for disposing all effects created within the scope.
--- @see https://github.com/littensy/charm?tab=readme-ov-file#effectscopecallback-detached
local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> ()
local self: EffectScope = {
local function effectScope(fn: () -> (), detached: boolean?): () -> ()
local e: EffectScope = {
flags = NONE,
cleanups = {},
}
if activeScope and not detached then
link(self, activeScope)
local prevSub = setActiveSub(e)
if prevSub and not detached then
link(e, prevSub, 0)
end
local prevSub = setCurrentSub(nil)
local prevScope = setCurrentScope(self)
local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn)
setCurrentScope(prevScope)
setCurrentSub(prevSub)
activeSub = prevSub
if not success then
error(result)
elseif result then
table.insert(self.cleanups, result)
table.insert(e.cleanups, result)
end
return effectStop(self)
return effectScopeOper(e)
end
--- Registers a cleanup function to the current active effect or effect scope.
@ -429,8 +399,6 @@ end
local function onCleanup(cleanup: () -> (), failSilently: boolean?)
if activeSub and (activeSub :: Effect).cleanups then
table.insert((activeSub :: Effect).cleanups, cleanup)
elseif activeScope then
table.insert(activeScope.cleanups, cleanup)
elseif not failSilently then
warn(debug.traceback("onCleanup() can only be called inside an effect or a scope.", 2))
end
@ -445,11 +413,9 @@ end
--- @param args Arguments to pass to the callback.
--- @return The return value of the callback.
local function untracked<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
local prevSub = setCurrentSub(nil)
local prevScope = setCurrentScope(nil)
local prevSub = setActiveSub(nil)
local results: { any } = { pcall(callback, ...) }
setCurrentSub(prevSub)
setCurrentScope(prevScope)
activeSub = prevSub
if not results[1] then
error(results[2])
end
@ -465,16 +431,20 @@ end
--- @param args Arguments to pass to the callback.
--- @return The return value of the callback.
local function peek<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): Results...
if not activeSub or bit32.btest(activeSub.flags, UNTRACKED) then
return callback(...)
else
activeSub.flags = bit32.bor(activeSub.flags, UNTRACKED)
if
activeSub
and bit32.band(activeSub.flags, PEEKING) == 0
and bit32.btest(activeSub.flags, bit32.bor(WATCHING, MUTABLE))
then
activeSub.flags = bit32.bor(activeSub.flags, PEEKING)
local results: { any } = { pcall(callback, ...) }
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(UNTRACKED))
activeSub.flags = bit32.band(activeSub.flags, bit32.bnot(PEEKING))
if not results[1] then
error(results[2])
end
return unpack(results, 2)
else
return callback(...)
end
end
@ -494,28 +464,51 @@ local function batched<Args..., Results...>(callback: (Args...) -> Results..., .
return unpack(results, 2)
end
local function notify(e: ReactiveNode)
local flags = e.flags
if not bit32.btest(flags, QUEUED) then
e.flags = bit32.bor(flags, QUEUED)
local subs = e.subs
if subs then
notify(subs.sub)
else
queuedEffectsLength += 1
queuedEffects[queuedEffectsLength] = e :: Effect
end
end
end
local reactiveSystem = createReactiveSystem({
update = update,
update = function(node: ReactiveNode): boolean
if node.depsTail then
return updateComputed(node :: Computed)
else
return updateSignal(node :: Signal)
end
end,
notify = notify,
unwatched = unwatched,
unwatched = function(node: ReactiveNode)
if bit32.band(node.flags, MUTABLE) == 0 then
effectScopeOper(node :: EffectScope)()
elseif node.depsTail then
node.depsTail = nil
node.flags = bit32.bor(MUTABLE, DIRTY)
purgeDeps(node)
end
end,
})
link = reactiveSystem.link
unlink = reactiveSystem.unlink
propagate = reactiveSystem.propagate
checkDirty = reactiveSystem.checkDirty
endTracking = reactiveSystem.endTracking
startTracking = reactiveSystem.startTracking
shallowPropagate = reactiveSystem.shallowPropagate
return {
ReactiveFlags = ReactiveFlags,
createReactiveSystem = createReactiveSystem,
getCurrentSub = getCurrentSub,
setCurrentSub = setCurrentSub,
getCurrentScope = getCurrentScope,
setCurrentScope = setCurrentScope,
onCleanup = onCleanup,
getActiveSub = getActiveSub,
setActiveSub = setActiveSub,
getBatchDepth = getBatchDepth,
startBatch = startBatch,
endBatch = endBatch,
@ -523,6 +516,7 @@ return {
computed = computed,
effect = effect,
effectScope = effectScope,
onCleanup = onCleanup,
untracked = untracked,
peek = peek,
batched = batched,

View file

@ -1,7 +1,7 @@
local core = require("./core")
local computed = core.computed
local setCurrentSub = core.setCurrentSub
local setActiveSub = core.setActiveSub
type Mapped =
& (<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item: VI, key: KI) -> (VO, KO)) -> () -> { [KO]: VO })
@ -12,9 +12,9 @@ local function mapped<KI, VI, KO, VO>(getter: () -> { [KI]: VI }, mapper: (item:
local keyCache: { [KI]: KO } = {}
local function map(value: VI, key: KI): (VO, KO)
local currentSub = setCurrentSub(nil)
local prevSub = setActiveSub(nil)
local success, valueOut, keyOut = pcall(mapper, value, key)
setCurrentSub(currentSub)
setActiveSub(prevSub)
if not success then
error(`mapped() transform function failed: {valueOut}`)

View file

@ -1,4 +1,4 @@
local function handleNoYield(thread: thread, ok: boolean, ...)
local function noYieldHandler(thread: thread, ok: boolean, ...)
if not ok then
return false, debug.traceback(thread, tostring((...)))
elseif coroutine.status(thread) ~= "dead" then
@ -10,7 +10,7 @@ end
local function noYield<Args..., Results...>(callback: (Args...) -> Results..., ...: Args...): (boolean, Results...)
local thread = coroutine.create(callback)
return handleNoYield(thread, coroutine.resume(thread, ...))
return noYieldHandler(thread, coroutine.resume(thread, ...))
end
local function deepFreeze(object: any)