v1.5.0 — Cleanup that always finishes
Twill’s own Bag closes newest first and runs to the end even when a step fails.
Before upgrading
Section titled “Before upgrading”Rename the type. Everything else keeps its name.
function MyService.OnPlayerReady(player, data, trove: Scope.Trove)function MyService.OnPlayerReady(player, data, bag: Scope.Bag)Add, Connect, Extend, Remove, Clean and Destroy are unchanged. Code
that uses only those needs no edit.
| Before | After |
|---|---|
bag:AttachToInstance(part) | bag:AttachTo(part) |
bag:Pop(x) | bag:Release(x) |
bag:Construct(Class, ...) | bag:Add(Class.new(...)) |
bag:WrapClean() | function() bag:Destroy() end |
bag:BindToRenderStep(n, p, f) | bag:Bind(n, p, f) |
bag:AddPromise(p) | bag:Add(p, "cancel") |
Twill.Trove.new() | Twill.Bag.new() |
Cleanup now runs newest first. Code that relied on the old forward order relied on behaviour that was never documented.
A failing cleanup no longer stops the bag
Section titled “A failing cleanup no longer stops the bag”The bundled library closed its entries in a loop that caught nothing, guarded by
a flag raised on entry. A cleanup that raised escaped the loop and left the flag
raised. Every remaining entry stayed held forever, and the bag cleaned nothing
again. Player bags are shared between systems, so one service’s failing
Destroy stopped every other service’s teardown.
Twill.Bag closes every entry inside its own pcall:
bag:Add(function() print("one") end)bag:Add(function() error("something went wrong") end)bag:Add(function() print("three") end)
bag:Clean()This prints three, reports the failure through Log,
prints one, and leaves the bag empty and usable.
Newest first
Section titled “Newest first”The last entry added is the first closed, so nothing is torn down after the thing it depends on:
bag:Add(connectionPool)bag:Add(connectionPool:Open()) -- closed first, while the pool still existsLinear teardown
Section titled “Linear teardown”Entries are taken out of the list before they run. This keeps closing linear while still allowing a cleanup to add to the bag it is closing. Measured in Studio:
| Entries | Rescanning | Bag |
|---|---|---|
| 100 | 0.012 ms | 0.015 ms |
| 1000 | 0.284 ms | 0.105 ms |
| 5000 | 9.92 ms | 0.544 ms |
Named entries
Section titled “Named entries”A name keeps one thing at a time. Adding under a name closes whatever held that name before:
local function retarget(model: Model) bag:Add(makeTween(model), "Cancel", "aim")endAnything with a closing method can be held by naming the method, including promises, so there is no promise dependency:
bag:Add(promise, "cancel")bag:Add(tween, "Cancel")bag:Add(sound, "Stop")Something with no way to close is refused at the Add call.
Other changes
Section titled “Other changes”-
A one-shot listener whose signal fired during the connect no longer leaves a dead connection in the bag.
-
A thread that cancels itself is deferred instead of failing silently.
-
Attaching to an instance that is already gone closes the bag at once instead of raising.
-
Count,IsEmptyandIsDestroyedlet a test assert that nothing leaked:Scope.Close(player)assert(bagFor(player):Count() == 0, "something outlived the player") -
Twill.Troveand the bundled Trove package are removed.
Full changelog
Section titled “Full changelog”Every change in this release is listed in the changelog.