From 925eba3b27c5885331aa44483456481cce2cb021 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Mon, 25 Mar 2024 14:17:51 -0400 Subject: [PATCH] Trove types --- README.md | 2 +- modules/trove/init.lua | 399 +++++++++++++++++++++++++++++---------- modules/trove/wally.toml | 2 +- 3 files changed, 299 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index f17c3f14..3dccf62a 100644 --- a/README.md +++ b/README.md @@ -28,5 +28,5 @@ | [TaskQueue](https://sleitnick.github.io/RbxUtil/api/TaskQueue) | `TaskQueue = "sleitnick/task-queue@1.0.0"` | Batches tasks that occur on the same execution step | | [Timer](https://sleitnick.github.io/RbxUtil/api/Timer) | `Timer = "sleitnick/timer@1.1.2"` | Timer class | | [Tree](https://sleitnick.github.io/RbxUtil/api/Tree) | `Tree = "sleitnick/tree@1.1.0"` | Utility functions for accessing instances in the game hierarchy | -| [Trove](https://sleitnick.github.io/RbxUtil/api/Trove) | `Trove = "sleitnick/trove@1.1.0"` | Trove class for tracking and cleaning up objects | +| [Trove](https://sleitnick.github.io/RbxUtil/api/Trove) | `Trove = "sleitnick/trove@1.2.0"` | Trove class for tracking and cleaning up objects | | [WaitFor](https://sleitnick.github.io/RbxUtil/api/WaitFor) | `WaitFor = "sleitnick/wait-for@1.0.0"` | WaitFor class for awaiting instances | diff --git a/modules/trove/init.lua b/modules/trove/init.lua index 19578c91..2fa501a9 100644 --- a/modules/trove/init.lua +++ b/modules/trove/init.lua @@ -1,23 +1,138 @@ --- Trove --- Stephen Leitnick --- October 16, 2021 +--!strict + +local RunService = game:GetService("RunService") + +export type Trove = { + Extend: (self: Trove) -> Trove, + Clone: (self: Trove, instance: T & Instance) -> T, + Construct: (self: Trove, class: Constructable, A...) -> T, + Connect: (self: Trove, signal: SignalLike | RBXScriptSignal, fn: (...any) -> ...any) -> ConnectionLike, + BindToRenderStep: (self: Trove, name: string, priority: number, fn: (dt: number) -> ()) -> (), + AddPromise: (self: Trove, promise: T & PromiseLike) -> T, + Add: (self: Trove, object: T & Trackable, cleanupMethod: string?) -> T, + Remove: (self: Trove, object: T & Trackable) -> boolean, + Clean: (self: Trove) -> (), + AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection, + Destroy: (self: Trove) -> (), +} + +type TroveInternal = Trove & { + _objects: { any }, + _cleaning: boolean, + _findAndRemoveFromObjects: (self: TroveInternal, object: any, cleanup: boolean) -> boolean, + _cleanupObject: (self: TroveInternal, object: any, cleanupMethod: string?) -> (), +} + +--[=[ + @within Trove + @type Trackable Instance | ConnectionLike | PromiseLike | thread | ((...any) -> ...any) | Destroyable | DestroyableLowercase | Disconnectable | DisconnectableLowercase + Represents all trackable objects by Trove. +]=] +export type Trackable = + Instance + | ConnectionLike + | PromiseLike + | thread + | ((...any) -> ...any) + | Destroyable + | DestroyableLowercase + | Disconnectable + | DisconnectableLowercase + +--[=[ + @within Trove + @interface ConnectionLike + .Connected boolean + .Disconnect (self) -> () +]=] +type ConnectionLike = { + Connected: boolean, + Disconnect: (self: ConnectionLike) -> (), +} + +--[=[ + @within Trove + @interface SignalLike + .Connect (self, callback: (...any) -> ...any) -> ConnectionLike + .Once (self, callback: (...any) -> ...any) -> ConnectionLike +]=] +type SignalLike = { + Connect: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike, + Once: (self: SignalLike, callback: (...any) -> ...any) -> ConnectionLike, +} + +--[=[ + @within Trove + @interface PromiseLike + .getStatus (self) -> string + .finally (self, callback: (...any) -> ...any) -> PromiseLike + .cancel (self) -> () +]=] +type PromiseLike = { + getStatus: (self: PromiseLike) -> string, + finally: (self: PromiseLike, callback: (...any) -> ...any) -> PromiseLike, + cancel: (self: PromiseLike) -> (), +} + +--[=[ + @within Trove + @type Constructable { new: (A...) -> T } | (A...) -> T +]=] +type Constructable = { new: (A...) -> T } | (A...) -> T + +--[=[ + @within Trove + @interface Destroyable + .disconnect (self) -> () +]=] +type Destroyable = { + Destroy: (self: Destroyable) -> (), +} + +--[=[ + @within Trove + @interface DestroyableLowercase + .disconnect (self) -> () +]=] +type DestroyableLowercase = { + destroy: (self: DestroyableLowercase) -> (), +} + +--[=[ + @within Trove + @interface Disconnectable + .disconnect (self) -> () +]=] +type Disconnectable = { + Disconnect: (self: Disconnectable) -> (), +} + +--[=[ + @within Trove + @interface DisconnectableLowercase + .disconnect (self) -> () +]=] +type DisconnectableLowercase = { + disconnect: (self: DisconnectableLowercase) -> (), +} local FN_MARKER = newproxy() local THREAD_MARKER = newproxy() -local GENERIC_OBJECT_CLEANUP_METHODS = { "Destroy", "Disconnect", "destroy", "disconnect" } +local GENERIC_OBJECT_CLEANUP_METHODS = table.freeze({ "Destroy", "Disconnect", "destroy", "disconnect" }) -local RunService = game:GetService("RunService") - -local function GetObjectCleanupFunction(object, cleanupMethod) +local function GetObjectCleanupFunction(object: any, cleanupMethod: string?) local t = typeof(object) + if t == "function" then return FN_MARKER elseif t == "thread" then return THREAD_MARKER end + if cleanupMethod then return cleanupMethod end + if t == "Instance" then return "Destroy" elseif t == "RBXScriptConnection" then @@ -29,17 +144,18 @@ local function GetObjectCleanupFunction(object, cleanupMethod) end end end - error("Failed to get cleanup function for object " .. t .. ": " .. tostring(object), 3) + + error(`failed to get cleanup function for object {t}: {object}`, 3) end -local function AssertPromiseLike(object) +local function AssertPromiseLike(object: any) if typeof(object) ~= "table" or typeof(object.getStatus) ~= "function" or typeof(object.finally) ~= "function" or typeof(object.cancel) ~= "function" then - error("Did not receive a Promise as an argument", 3) + error("did not receive a promise as an argument", 3) end end @@ -54,55 +170,106 @@ Trove.__index = Trove --[=[ @return Trove Constructs a Trove object. + + ```lua + local trove = Trove.new() + ``` ]=] -function Trove.new() +function Trove.new(): Trove local self = setmetatable({}, Trove) + self._objects = {} self._cleaning = false - return self + + return (self :: any) :: Trove end --[=[ - @return Trove - Creates and adds another trove to itself. This is just shorthand - for `trove:Construct(Trove)`. This is useful for contexts where - the trove object is present, but the class itself isn't. + @method Add + @within Trove + @param object any -- Object to track + @param cleanupMethod string? -- Optional cleanup name override + @return object: any + Adds an object to the trove. Once the trove is cleaned or + destroyed, the object will also be cleaned up. - :::note - This does _not_ clone the trove. In other words, the objects in the - trove are not given to the new constructed trove. This is simply to - construct a new Trove and add it as an object to track. - ::: + The following types are accepted (e.g. `typeof(object)`): + + | Type | Cleanup | + | ---- | ------- | + | `Instance` | `object:Destroy()` | + | `RBXScriptConnection` | `object:Disconnect()` | + | `function` | `object()` | + | `thread` | `task.cancel(object)` | + | `table` | `object:Destroy()` _or_ `object:Disconnect()` _or_ `object:destroy()` _or_ `object:disconnect()` | + | `table` with `cleanupMethod` | `object:()` | + + Returns the object added. ```lua - local trove = Trove.new() - local subTrove = trove:Extend() + -- Add a part to the trove, then destroy the trove, + -- which will also destroy the part: + local part = Instance.new("Part") + trove:Add(part) + trove:Destroy() - trove:Clean() -- Cleans up the subTrove too + -- Add a function to the trove: + trove:Add(function() + print("Cleanup!") + end) + trove:Destroy() + + -- Standard cleanup from table: + local tbl = {} + function tbl:Destroy() + print("Cleanup") + end + trove:Add(tbl) + + -- Custom cleanup from table: + local tbl = {} + function tbl:DoSomething() + print("Do something on cleanup") + end + trove:Add(tbl, "DoSomething") ``` ]=] -function Trove:Extend() +function Trove.Add(self: TroveInternal, object: Trackable, cleanupMethod: string?): any if self._cleaning then - error("Cannot call trove:Extend() while cleaning", 2) + error("cannot call trove:Add() while cleaning", 2) end - return self:Construct(Trove) + + local cleanup = GetObjectCleanupFunction(object, cleanupMethod) + table.insert(self._objects, { object, cleanup }) + + return object end --[=[ + @method Clone + @within Trove + @return Instance Clones the given instance and adds it to the trove. Shorthand for `trove:Add(instance:Clone())`. + + ```lua + local clonedPart = trove:Clone(somePart) + ``` ]=] -function Trove:Clone(instance: Instance): Instance +function Trove.Clone(self: TroveInternal, instance: Instance): Instance if self._cleaning then - error("Cannot call trove:Clone() while cleaning", 2) + error("cannot call trove:Clone() while cleaning", 2) end + return self:Add(instance:Clone()) end --[=[ - @param class table | (...any) -> any - @param ... any - @return any + @method Construct + @within Trove + @param class { new(Args...) -> T } | (Args...) -> T + @param ... Args... + @return T Constructs a new object from either the table or function given. @@ -132,21 +299,25 @@ end local part = trove:Construct(Instance, "Part") ``` ]=] -function Trove:Construct(class, ...) +function Trove.Construct(self: TroveInternal, class: Constructable, ...: A...) if self._cleaning then error("Cannot call trove:Construct() while cleaning", 2) end + local object = nil local t = type(class) if t == "table" then - object = class.new(...) + object = (class :: any).new(...) elseif t == "function" then - object = class(...) + object = (class :: any)(...) end + return self:Add(object) end --[=[ + @method Connect + @within Trove @param signal RBXScriptSignal @param fn (...: any) -> () @return RBXScriptConnection @@ -161,14 +332,17 @@ end end) ``` ]=] -function Trove:Connect(signal, fn) +function Trove.Connect(self: TroveInternal, signal: SignalLike, fn: (...any) -> ...any) if self._cleaning then error("Cannot call trove:Connect() while cleaning", 2) end + return self:Add(signal:Connect(fn)) end --[=[ + @method BindToRenderStep + @within Trove @param name string @param priority number @param fn (dt: number) -> () @@ -181,17 +355,21 @@ end end) ``` ]=] -function Trove:BindToRenderStep(name: string, priority: number, fn: (dt: number) -> ()) +function Trove.BindToRenderStep(self: TroveInternal, name: string, priority: number, fn: (dt: number) -> ()) if self._cleaning then - error("Cannot call trove:BindToRenderStep() while cleaning", 2) + error("cannot call trove:BindToRenderStep() while cleaning", 2) end + RunService:BindToRenderStep(name, priority, fn) + self:Add(function() RunService:UnbindFromRenderStep(name) end) end --[=[ + @method AddPromise + @within Trove @param promise Promise @return Promise Gives the promise to the trove, which will cancel the promise if the trove is cleaned up or if the promise @@ -214,11 +392,12 @@ end This is only compatible with the [roblox-lua-promise](https://eryn.io/roblox-lua-promise/) library, version 4. ::: ]=] -function Trove:AddPromise(promise) +function Trove.AddPromise(self: TroveInternal, promise: PromiseLike) if self._cleaning then - error("Cannot call trove:AddPromise() while cleaning", 2) + error("cannot call trove:AddPromise() while cleaning", 2) end AssertPromiseLike(promise) + if promise:getStatus() == "Started" then promise:finally(function() if self._cleaning then @@ -226,120 +405,110 @@ function Trove:AddPromise(promise) end self:_findAndRemoveFromObjects(promise, false) end) + self:Add(promise, "cancel") end + return promise end --[=[ - @param object any -- Object to track - @param cleanupMethod string? -- Optional cleanup name override - @return object: any - Adds an object to the trove. Once the trove is cleaned or - destroyed, the object will also be cleaned up. - - The following types are accepted (e.g. `typeof(object)`): - - | Type | Cleanup | - | ---- | ------- | - | `Instance` | `object:Destroy()` | - | `RBXScriptConnection` | `object:Disconnect()` | - | `function` | `object()` | - | `thread` | `task.cancel(object)` | - | `table` | `object:Destroy()` _or_ `object:Disconnect()` _or_ `object:destroy()` _or_ `object:disconnect()` | - | `table` with `cleanupMethod` | `object:()` | - - Returns the object added. + @method Remove + @within Trove + @param object any + Removes the object from the Trove and cleans it up. ```lua - -- Add a part to the trove, then destroy the trove, - -- which will also destroy the part: local part = Instance.new("Part") trove:Add(part) - trove:Destroy() - - -- Add a function to the trove: - trove:Add(function() - print("Cleanup!") - end) - trove:Destroy() - - -- Standard cleanup from table: - local tbl = {} - function tbl:Destroy() - print("Cleanup") - end - trove:Add(tbl) - - -- Custom cleanup from table: - local tbl = {} - function tbl:DoSomething() - print("Do something on cleanup") - end - trove:Add(tbl, "DoSomething") + trove:Remove(part) ``` ]=] -function Trove:Add(object: any, cleanupMethod: string?): any +function Trove.Remove(self: TroveInternal, object: Trackable): boolean if self._cleaning then - error("Cannot call trove:Add() while cleaning", 2) + error("cannot call trove:Remove() while cleaning", 2) end - local cleanup = GetObjectCleanupFunction(object, cleanupMethod) - table.insert(self._objects, { object, cleanup }) - return object + + return self:_findAndRemoveFromObjects(object, true) end --[=[ - @param object any -- Object to remove - Removes the object from the Trove and cleans it up. + @method Extend + @within Trove + @return Trove + Creates and adds another trove to itself. This is just shorthand + for `trove:Construct(Trove)`. This is useful for contexts where + the trove object is present, but the class itself isn't. + + :::note + This does _not_ clone the trove. In other words, the objects in the + trove are not given to the new constructed trove. This is simply to + construct a new Trove and add it as an object to track. + ::: ```lua - local part = Instance.new("Part") - trove:Add(part) - trove:Remove(part) + local trove = Trove.new() + local subTrove = trove:Extend() + + trove:Clean() -- Cleans up the subTrove too ``` ]=] -function Trove:Remove(object: any): boolean +function Trove.Extend(self: TroveInternal) if self._cleaning then - error("Cannot call trove:Remove() while cleaning", 2) + error("cannot call trove:Extend() while cleaning", 2) end - return self:_findAndRemoveFromObjects(object, true) + + return self:Construct(Trove) end --[=[ + @method Clean + @within Trove Cleans up all objects in the trove. This is similar to calling `Remove` on each object within the trove. The ordering of the objects removed is _not_ guaranteed. + + ```lua + trove:Clean() + ``` ]=] -function Trove:Clean() +function Trove.Clean(self: TroveInternal) if self._cleaning then return end + self._cleaning = true + for _, obj in self._objects do self:_cleanupObject(obj[1], obj[2]) end + table.clear(self._objects) self._cleaning = false end -function Trove:_findAndRemoveFromObjects(object: any, cleanup: boolean): boolean +function Trove._findAndRemoveFromObjects(self: TroveInternal, object: any, cleanup: boolean): boolean local objects = self._objects + for i, obj in ipairs(objects) do if obj[1] == object then local n = #objects objects[i] = objects[n] objects[n] = nil + if cleanup then self:_cleanupObject(obj[1], obj[2]) end + return true end end + return false end -function Trove:_cleanupObject(object, cleanupMethod) +function Trove._cleanupObject(_self: TroveInternal, object: any, cleanupMethod: string?) if cleanupMethod == FN_MARKER then object() elseif cleanupMethod == THREAD_MARKER then @@ -350,6 +519,8 @@ function Trove:_cleanupObject(object, cleanupMethod) end --[=[ + @method AttachToInstance + @within Trove @param instance Instance @return RBXScriptConnection Attaches the trove to a Roblox instance. Once this @@ -357,27 +528,51 @@ end parent set to `nil`), the trove will automatically clean up. + This inverses the ownership of the Trove object, and should + only be used when necessary. In other words, the attached + instance dictates when the trove is cleaned up, rather than + the trove dictating the cleanup of the instance. + :::caution Will throw an error if `instance` is not a descendant of the game hierarchy. ::: + + ```lua + trove:AttachToInstance(somePart) + trove:Add(function() + print("Cleaned") + end) + + -- Destroying the part will cause the trove to clean up, thus "Cleaned" printed: + somePart:Destroy() + ``` ]=] -function Trove:AttachToInstance(instance: Instance) +function Trove.AttachToInstance(self: TroveInternal, instance: Instance) if self._cleaning then - error("Cannot call trove:AttachToInstance() while cleaning", 2) + error("cannot call trove:AttachToInstance() while cleaning", 2) elseif not instance:IsDescendantOf(game) then - error("Instance is not a descendant of the game hierarchy", 2) + error("instance is not a descendant of the game hierarchy", 2) end + return self:Connect(instance.Destroying, function() self:Destroy() end) end --[=[ + @method Destroy + @within Trove Alias for `trove:Clean()`. + + ```lua + trove:Destroy() + ``` ]=] -function Trove:Destroy() +function Trove.Destroy(self: TroveInternal) self:Clean() end -return Trove +return { + new = Trove.new, +} diff --git a/modules/trove/wally.toml b/modules/trove/wally.toml index 5960e8f1..00ebcd5e 100644 --- a/modules/trove/wally.toml +++ b/modules/trove/wally.toml @@ -1,7 +1,7 @@ [package] name = "sleitnick/trove" description = "Trove class for tracking and cleaning up objects" -version = "1.1.0" +version = "1.2.0" license = "MIT" authors = ["Stephen Leitnick"] registry = "https://github.com/UpliftGames/wally-index"