Limit cleanup() calls to once per scope

This commit is contained in:
Aaron Smith 2023-08-22 11:46:56 +01:00
parent 2aebaf5abb
commit 0105e33fac
5 changed files with 63 additions and 5 deletions

View file

@ -2,7 +2,7 @@
## cleanup()
Runs a callback anytime a reactive scope is re-ran.
Runs a callback anytime a function scope is re-ran.
- **Type**
@ -10,6 +10,19 @@ Runs a callback anytime a reactive scope is re-ran.
function cleanup(callback: () -> ())
```
- **Details**
The primary purpose of this function is to provide a means of cleaning up
side effects caused by source updates and `watch()` updates.
The stack is inspected to find the function that calls `cleanup()`. The
callback passed is called anytime the caller is re-ran, and when the caller
finally garbage collects.
::: warning
Only one `cleanup()` call is allowed per function scope.
:::
- **Example**
```lua

View file

@ -17,6 +17,7 @@ Currently, strict mode will:
4. Checks for `indexes()` and `values()` returning primitive values.
5. Checks for duplicate nested properties at same depth.
6. Better error reporting and stack traces.
7. Checks for multiple `cleanup()` calls in the same function scope.
By rerunning sources and watchers, any side-effects are made more apparent.
This also helps ensure that cleanups are being handled correctly.

View file

@ -1,17 +1,23 @@
if not game then script = require "test/relative-string" end
local flags = require(script.Parent.flags)
local throw = require(script.Parent.throw)
local ref_to_id = {} :: { [string]: number }
local id_to_ref = {} :: { [number]: string }
local cleanup_callbacks = {} :: { [number]: () -> () } -- always dense
local cleanup_lifetime = {} :: { [number]: unknown } -- can be sparse
setmetatable(cleanup_lifetime :: any, { __mode = "v" })
local debug_caller_to_line = {} :: { [() -> ()]: number }
setmetatable(debug_caller_to_line, { __mode = "k" })
local manual_mode = {
caller = false :: false | () -> (),
callbacks = {} :: { () -> () }
}
-- todo: rare case where mem address is reused by another function on same line
-- todo: rare case where mem address is reused by another function
-- does this case handle itself?
local function cleanup_ref(ref: string, lifetime: unknown, callback: () -> ())
@ -32,12 +38,20 @@ end
local function cleanup(callback: () -> ())
local lifetime = debug.info(2, "f") -- `caller of cleanup() is lifetime of cleanup`
local line = debug.info(2, "l")
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) .. line
local ref = tostring(lifetime)
cleanup_ref(ref, lifetime, callback)
end
end

View file

@ -547,6 +547,26 @@ TEST("cleanup()", function()
CHECK(objB.cleaned == 2)
end
-- this is not allowed, test to verify behavior anyways
do CASE "multiple cleanup"
local state = source(1)
local queue = {}
watch(function()
state()
cleanup(function() table.insert(queue, 1) end)
cleanup(function() table.insert(queue, 2) end)
end)
CHECK(testkit.seq(queue, { 1 }))
state(2)
CHECK(testkit.seq(queue, { 1, 2, 1 }))
state(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1 }))
end
--[[
do CASE "multiple cleanup"
local state = source(1)
@ -574,6 +594,7 @@ TEST("cleanup()", function()
--testkit.print2(queue)
--CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1, 2 }))
end
]]
end)
TEST("create()", function()
@ -1308,6 +1329,7 @@ TEST("strict", function()
local derive = vide.derive
local watch = vide.watch
local indexes, values = vide.indexes, vide.values
local cleanup = vide.cleanup
do CASE "error on derived callback yield"
local state = source(1)
@ -1410,6 +1432,15 @@ TEST("strict", function()
CHECK(ok)
end
do CASE "multiple cleanup per scope"
local ok = pcall(function()
cleanup(function() end)
cleanup(function() end)
end)
CHECK(not ok)
end
end)
local ok = FINISH()

View file

@ -1,7 +1,6 @@
# todo
- cleanup codebase
- limit `cleanup()` call to once per function scope?
- better error reporting and stack traces in strict mode
- auto-enable of strict mode depending on compiler optimizaton level
- check smoothness of spring at high frequency updates