###
[Janitor]
TruScript port of howmanysmall's Janitor(originally by Validark)
Janitor holds things that need cleaning up later (instances, connections, functions, other objects), and cleans them all with one call. Objects are made of closures, so call members with a dot: janitor.Add(...)
TruScript differences from Roblox:
- The cleanup method can be true (call the object), "Destroy", "Disconnect", "Cancel", "Stop", any other method name of a table, or a function that receives the object, such as func(tween): tween.Cancel()
Engine objects cannot be indexed by name in TruScript, so use a function for their other methods.
- Left out, the method is guessed, so functions are called, connections are disconnected, and everything else is destroyed.
- RemoveList and RemoveListNoClean take an array of indices instead of variadic arguments
- There is no __call, but janitor.Cleanup is already bound, so pass it anywhere a callback is expected.
- Threads and Promises are not part of TruScript, so AddPromise and thread cancelling are left out.
- Cleanup runs newest-first, and one failing cleanup is reported with warn() instead of stopping the rest
USAGES:
local Janitor = import("/Libraries/Janitor")
Example usage:
local janitor = Janitor.New()
janitor.Add(new Part() { Anchored = true, Parent = world }, "Destroy")
janitor.Add(part.Touched.Connect(on_touch))
janitor.Add(func(): print("cleaned up"), true)
janitor.Add(Tween.Create(panel, params, goals), func(tween): tween.Cancel(), "CurrentTween")
janitor.Remove("CurrentTween")
janitor.LinkToInstance(character)
janitor.Cleanup()
janitor.Destroy()
###
local Janitor = {}
local janitor_mark = {}
local link_key = {}
local call_object = 0
local destroy_object = 1
local disconnect_object = 2
local cancel_object = 3
local stop_object = 4
local named_method = 5
local cleanup_function = 6
#bs code for old typedefaults
local guess_kind = func(object):
local kind = destroy_object
try:
if object.IsA:
kind = destroy_object
elif object.Disconnect:
kind = disconnect_object
catch not_an_instance:
try:
if object.Disconnect:
kind = disconnect_object
catch not_a_connection:
kind = call_object
return kind
#Turns Add into a kind number, warning early when an object lacks the method
local resolve_kind = func(object, method_name):
if method_name == nil:
return guess_kind(object)
if method_name == true:
return call_object
local kind = named_method
if method_name == "Destroy":
kind = destroy_object
elif method_name == "Disconnect":
kind = disconnect_object
elif method_name == "Cancel":
kind = cancel_object
elif method_name == "Stop":
kind = stop_object
else:
try:
local name_length = method_name.length
catch not_a_name:
kind = cleanup_function
if kind != named_method:
return kind
try:
if not object[method_name]:
warn("Janitor: " + tostring(object) + " has no method " + method_name + ", are you sure you want to add it?")
catch no_such_member:
warn("Janitor: " + tostring(object) + " cannot be cleaned with " + method_name + ", pass a function instead")
return kind
local run_cleanup = func(janitor, object, kind, extra):
if kind == call_object:
object()
elif kind == destroy_object:
if janitor.SuppressInstanceReDestroy:
try:
object.Destroy()
catch already_destroyed:
kind = destroy_object
else:
object.Destroy()
elif kind == disconnect_object:
object.Disconnect()
elif kind == cancel_object:
object.Cancel()
elif kind == stop_object:
object.Stop()
elif kind == named_method:
object[extra]()
else:
extra(object)
# Janitor.Is(object) -> true when object was made by Janitor.New
Janitor.Is = func(object):
local result = false
try:
result = object[janitor_mark] == true
catch not_a_table:
result = false
return result
# Janitor.New() -> a new, empty janitor
Janitor.New = func():
local janitor = { ClassName = "Janitor", CurrentlyCleaning = false, SuppressInstanceReDestroy = false }
janitor[janitor_mark] = true
# data-oriented storage: parallel arrays hold every object with how to clean it
# slots maps an object to its array position, and indices maps a namespace index to its object.
local objects = []
local kinds = []
local extras = []
local slots = {}
local indices = {}
local drop_slot = func(slot):
local last = objects.length - 1
table.remove(slots, objects[slot])
if slot != last:
local moved = objects[last]
objects[slot] = moved
kinds[slot] = kinds[last]
extras[slot] = extras[last]
slots[moved] = slot
objects.pop()
kinds.pop()
extras.pop()
local remove_index = func(index, clean):
if index == nil or not table.has(indices, index):
return janitor
local object = indices[index]
table.remove(indices, index)
local slot = slots[object]
if slot == nil:
return janitor
local kind = kinds[slot]
local extra = extras[slot]
drop_slot(slot)
if clean:
run_cleanup(janitor, object, kind, extra)
return janitor
# janitor.Add(object, method_name?, index?) -> object
# method_name - true, a method name or a cleanup function. index: an optional namespace that replaces(and cleans) whatever was stored under it before, and can later be passed to Remove or Get
janitor.Add = func(object, method_name, index):
if object == nil:
throw "Janitor.Add was given nil"
if index != nil:
remove_index(index, true)
indices[index] = object
local kind = resolve_kind(object, method_name)
local extra = nil
if kind == named_method or kind == cleanup_function:
extra = method_name
local slot = slots[object]
if slot != nil:
kinds[slot] = kind
extras[slot] = extra
else:
slots[object] = objects.length
objects.push(object)
kinds.push(kind)
extras.push(extra)
return object
# janitor.AddObject(constructor, method_name?, index?, a, b, c, d) -> constructor.New(a, b, c, d), added
janitor.AddObject = func(constructor, method_name, index, a, b, c, d):
return janitor.Add(constructor.New(a, b, c, d), method_name, index)
# janitor.Remove(index) - cleans the object stored under index and forgets it
janitor.Remove = func(index):
return remove_index(index, true)
# janitor.RemoveNoClean(index) - forgets the object stored under index without cleaning it
janitor.RemoveNoClean = func(index):
return remove_index(index, false)
# janitor.RemoveList([index, ...]) - cleans several namespaces at once
janitor.RemoveList = func(index_list):
for index in index_list:
remove_index(index, true)
return janitor
# janitor.RemoveListNoClean([index, ...]) - forgets several namespaces without cleaning them
janitor.RemoveListNoClean = func(index_list):
for index in index_list:
remove_index(index, false)
return janitor
# janitor.Get(index) -> the object stored under index, or nil
janitor.Get = func(index):
return indices[index]
# janitor.GetAll() -> a copy of every index and its object
janitor.GetAll = func():
return table.map(indices, expr(object): object)
# janitor.Cleanup() -cleans everything, newest first, and empties the janitor so it can be reused
janitor.Cleanup = func():
if janitor.CurrentlyCleaning:
return
janitor.CurrentlyCleaning = true
while objects.length > 0:
local object = objects.pop()
local kind = kinds.pop()
local extra = extras.pop()
table.remove(slots, object)
try:
run_cleanup(janitor, object, kind, extra)
catch problem:
warn("Janitor: a cleanup failed: " + tostring(problem))
table.clear(indices)
janitor.CurrentlyCleaning = false
# janitor.Destroy() - cleans everything and makes the janitor unusable
janitor.Destroy = func():
janitor.Cleanup()
table.clear(janitor)
# janitor.LinkToInstance(instance, allow_multiple?) -> connection
# cleans Janitor when the instance is destroyed. Without allow_multiple, a new link replaces the old one.
janitor.LinkToInstance = func(instance, allow_multiple):
local key = link_key
if allow_multiple:
key = {}
return janitor.Add(instance.Destroyed.Connect(janitor.Cleanup), "Disconnect", key)
# janitor.LinkToInstances([instance, ...]) -> a new janitor holding the links, so they can be undone together
janitor.LinkToInstances = func(instance_list):
local links = Janitor.New()
for instance in instance_list:
local is_instance = false
try:
is_instance = instance.IsA("Instance")
catch not_an_instance:
is_instance = false
if is_instance:
links.Add(janitor.LinkToInstance(instance, true), "Disconnect")
return links
return janitor
return Janitor
This is very cool! I’d like to hear about your thought process behind creating object-oriented data structures such as this one in TruScript.
Currently TruScript is designed as a procedural language in terms of what users can program, but I want to add support for user classes soon. If you have anything to share about what you’d like to see, I’m very curious.
Since truscript doesn’t have classes yet, I built each object as like a closure, where the constructor makes a table and keeps a state in locals and attaches functions that capture them. There were a few issues when translating the original system from luau to truscript. There’s no “typeof” method, so I used members with try/catch to tell instances, connections and functions apart. Also there are no varargs or multiple returns, so it has limit of 6 values (abcdef) from what I tested
For plr classes, what I’d like to be added personally is maybe methods shared across instances with a self kinda style so every object doesn’t carry its own copy of every function. A built in way to check what class a value is would provably help as well, and varargs would let things like Fire forward any number of arguments
Varargs is a great suggestion. Class functions will be optimized to compile statically and stored in the type header object (even member functions) so they will provide less overhead than wrapping a new closure in each table with your implementation. I will take your other requests into consideration
Thank you very much for your feedback!