mirror of
https://github.com/centau/vide.git
synced 2026-08-20 23:01:37 +00:00
Merge reactive scope refactor
This commit is contained in:
parent
0e439f084f
commit
efc4798ddb
48 changed files with 2750 additions and 1949 deletions
129
src/cleanup.luau
129
src/cleanup.luau
|
|
@ -1,129 +1,18 @@
|
|||
if not game then script = require "test/relative-string" end
|
||||
|
||||
local flags = require(script.Parent.flags)
|
||||
local throw = require(script.Parent.throw)
|
||||
local graph = require(script.Parent.graph)
|
||||
local get_scope = graph.get_scope
|
||||
local add_cleanup = graph.add_cleanup
|
||||
|
||||
--[[
|
||||
|
||||
Cleanups associate a callback with an arbitrary value with an unknown lifetime.
|
||||
Anytime a new callback is registered with a value that already has one registered,
|
||||
the registered callback is ran and then replaced with the new one.
|
||||
|
||||
When the value is eventually garbage collected, Vide checks for callbacks
|
||||
without an associated value, which it will then run and clear, recycling its
|
||||
cleanup id.
|
||||
|
||||
By default the arbitrary value is the function object that calls `cleanup()`.
|
||||
There are exceptions such as with `indexes()` and `values()` where the
|
||||
arbitrary value is manually set to be the new source created instead of the
|
||||
caller, as the same caller can be used to create multiple new objects.
|
||||
|
||||
todo: remove need for ref to id maps?
|
||||
|
||||
]]
|
||||
|
||||
-- maps a ref to cleanup id
|
||||
local ref_to_id = {} :: { [string]: number }
|
||||
-- maps a cleanup id to a ref
|
||||
local id_to_ref = {} :: { [number]: string }
|
||||
-- array of all cleanup callbacks
|
||||
local cleanup_callbacks = {} :: { [number]: () -> () } -- always dense
|
||||
-- weak array of all cleanup lifetimes
|
||||
local cleanup_lifetime = {} :: { [number]: unknown } -- can be sparse
|
||||
setmetatable(cleanup_lifetime :: any, { __mode = "v" })
|
||||
|
||||
-- detects in strict mode when multiple cleanups are registered in the same scope
|
||||
local debug_caller_to_line = {} :: { [() -> ()]: number }
|
||||
setmetatable(debug_caller_to_line, { __mode = "k" })
|
||||
|
||||
-- when active, cleanup callbacks are not automatically registered but are
|
||||
-- added to an array for manual registering internally
|
||||
local manual_mode = {
|
||||
caller = false :: false | () -> (),
|
||||
callbacks = {} :: { () -> () }
|
||||
}
|
||||
|
||||
-- todo: rare case where mem address is reused by another function
|
||||
-- does this case handle itself?
|
||||
|
||||
-- registers a callback with the given lifetime using the given ref
|
||||
local function cleanup_ref(ref: string, lifetime: unknown, callback: () -> ())
|
||||
local id = ref_to_id[ref]
|
||||
|
||||
if id then -- invoke previously registered callback then register new one
|
||||
cleanup_callbacks[id]()
|
||||
cleanup_lifetime[id] = lifetime -- rare case where ref is reused while lifetime is nil
|
||||
else -- no previously registered callback, add and register new one
|
||||
id = #cleanup_callbacks + 1
|
||||
ref_to_id[ref] = id
|
||||
id_to_ref[id :: any] = ref -- todo
|
||||
cleanup_lifetime[id :: any] = lifetime -- todo
|
||||
end
|
||||
|
||||
cleanup_callbacks[id] = callback
|
||||
end
|
||||
|
||||
-- registers a callback with its caller as the lifetime, and caller address as the ref
|
||||
local function cleanup(callback: () -> ())
|
||||
local lifetime = debug.info(2, "f") -- `caller of cleanup() is lifetime of cleanup`
|
||||
local scope = get_scope()
|
||||
if not scope then
|
||||
throw("cannot cleanup in a non-reactive scope")
|
||||
end; assert(scope)
|
||||
|
||||
if flags.strict then
|
||||
local line = debug.info(2, "l")
|
||||
local cur_line = debug_caller_to_line[lifetime]
|
||||
if cur_line and cur_line ~= line then
|
||||
throw "only one cleanup call is allowed per function scope"
|
||||
end
|
||||
debug_caller_to_line[lifetime] = line
|
||||
end
|
||||
|
||||
if manual_mode.caller == lifetime then
|
||||
table.insert(manual_mode.callbacks, callback)
|
||||
else
|
||||
local ref = tostring(lifetime)
|
||||
cleanup_ref(ref, lifetime, callback)
|
||||
end
|
||||
add_cleanup(scope, callback)
|
||||
end
|
||||
|
||||
local function clean_garbage()
|
||||
for id = #cleanup_callbacks, 1, -1 do
|
||||
if cleanup_lifetime[id] == nil then -- lifetime was garbage collected
|
||||
local callback = cleanup_callbacks[id]
|
||||
return cleanup
|
||||
|
||||
do -- swap and pop
|
||||
local max_id = #cleanup_callbacks
|
||||
|
||||
cleanup_callbacks[id] = cleanup_callbacks[max_id]
|
||||
cleanup_callbacks[max_id] = nil
|
||||
|
||||
cleanup_lifetime[id] = cleanup_lifetime[max_id]
|
||||
cleanup_lifetime[max_id] = nil
|
||||
|
||||
local ref = id_to_ref[id]
|
||||
local max_ref = id_to_ref[max_id]
|
||||
|
||||
id_to_ref[id] = max_ref
|
||||
id_to_ref[max_id] = nil
|
||||
|
||||
ref_to_id[max_ref] = id
|
||||
ref_to_id[ref] = nil
|
||||
end
|
||||
|
||||
local ok, err: string? = pcall(callback)
|
||||
if not ok then warn(`error occured during cleanup: {err}`) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local manual_cleanup_mode = function(caller: () -> ()?)
|
||||
if caller == nil then
|
||||
local clone = table.clone(manual_mode.callbacks)
|
||||
manual_mode.caller = false
|
||||
table.clear(manual_mode.callbacks)
|
||||
return clone
|
||||
else
|
||||
manual_mode.caller = caller
|
||||
end
|
||||
return manual_mode.callbacks
|
||||
end :: ( (caller: (...any) -> ()) -> () ) & ( (nil) -> { () -> () } )
|
||||
|
||||
return function() return cleanup, clean_garbage, manual_cleanup_mode, cleanup_ref end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue