Lifecycle And Cleanup
Rivet gives every Unit a small managed lifecycle:
require Unit modules
normalize metadata
prepare runtime fields
sort dependency graph
setup networking
run every Init
run every Start
later: destroy in reverse order
You usually write only three lifecycle methods: Init, Start, and Destroy.
function Unit:Init()
end
function Unit:Start()
end
function Unit:Destroy()
end
All three are optional. A Unit can be useful with none of them if it only exposes methods or static behavior.
Prepare
Prepare is Rivet's internal step. You do not write a Prepare method.
During prepare, Rivet attaches:
self.Cleanself:Get(id)- normalized
self.Id - normalized
self.Dependencies - normalized
self.Surfaces
Signals also get server helper tables during networking setup. If a Unit declares a Client Signal, self.Client.SignalName exists by the time your lifecycle methods run.
Init
Use Init to build state and capture dependencies.
function Inventory:Init()
self.Data = self:Get("Data")
self.ItemsByPlayer = {}
end
Use Init for:
- dependency lookup
- private state tables
- small validation
- wiring simple references
- registering cleanup tasks that do not require all Units to start
Avoid long loops, yielding remote calls, or gameplay activation in Init. Rivet runs Init as part of startup, and all Units must finish initialization before any Unit can start.
Start
Use Start for work that begins the Unit's active behavior.
function Inventory:Start()
self.Clean:Add(Players.PlayerRemoving:Connect(function(player)
self.ItemsByPlayer[player] = nil
end))
end
Use Start for:
- connecting Roblox events
- starting loops or timers
- loading current players
- scheduling recurring work
- firing initial Signals
- interacting with other Units after all
Initmethods are complete
Rivet runs all Init methods before any Start method. If a Unit exists in the runtime, its Init has already finished by the time your Start method runs.
Destroy
Use Destroy for custom shutdown.
function Inventory:Destroy()
table.clear(self.ItemsByPlayer)
end
After Destroy runs, Rivet calls self.Clean:Cleanup() for that Unit. Destroy order is reverse dependency order.
Inventory:Destroy()
Inventory.Clean:Cleanup()
Data:Destroy()
Data.Clean:Cleanup()
If a Unit's Destroy or cleanup fails, Rivet records the first failure and continues attempting to destroy the rest of the runtime. After cleanup, it reports the failure.
Clean
Clean is Rivet's built-in cleanup helper. Every Unit receives self.Clean, and you can also create standalone cleaners with Rivet.Clean.new().
local clean = Rivet.Clean.new()
Add cleanup tasks near the work that creates them:
function Session:Start()
local connection = Players.PlayerRemoving:Connect(function(player)
print(player.Name, "left")
end)
self.Clean:Add(connection)
end
When cleanup runs, tasks are cleaned in reverse add order. Last in, first out.
Supported Cleanup Tasks
Clean:Add(object, methodName?) understands the cleanup shapes Roblox code uses most often. Functions are called, Instances are destroyed, connections are disconnected, and tables with Destroy, Disconnect, or Cleanup methods have the matching method called. If an object needs a project-specific cleanup method, pass that method name as the second argument.
self.Clean:Add(function()
print("manual cleanup")
end)
self.Clean:Add(workspace.TempFolder)
self.Clean:Add(connection)
self.Clean:Add(resource, "Close")
Clean:Add(nil) throws. If a task is optional, check it before adding it.
Remove Without Cleaning
Clean:Remove(object) stops tracking a task and returns whether it found it.
local connection = self.Clean:Add(signal:Connect(callback))
if shouldKeepAlive then
self.Clean:Remove(connection)
end
Remove does not disconnect or destroy the object. It only removes the pending cleanup task.
Manual Cleanup
You can call cleanup manually:
self.Clean:Cleanup()
Destroy is an alias:
self.Clean:Destroy()
After cleanup, the cleaner removes tasks as it runs them. Calling cleanup again does not re-run already cleaned tasks.
Full Lifecycle Example
--!strict
local Players = game:GetService("Players")
local Session = {}
Session.Id = "Session"
Session.Dependencies = { "Data" }
function Session:Init()
self.Data = self:Get("Data")
self.ActiveSessions = {}
end
function Session:Start()
self.Clean:Add(Players.PlayerAdded:Connect(function(player)
self.ActiveSessions[player] = {
StartedAt = os.clock(),
}
end))
self.Clean:Add(Players.PlayerRemoving:Connect(function(player)
self.ActiveSessions[player] = nil
end))
end
function Session:Destroy()
table.clear(self.ActiveSessions)
end
return Session
This Unit makes the lifecycle obvious:
Init stores dependencies and state. Start connects events and adds the connections to cleanup. Destroy clears the Unit's owned state. After that, Clean disconnects the events that were registered during Start.
Error Handling
If Init, Start, or Destroy errors, Rivet wraps the message with the Unit id and lifecycle method.
Rivet unit "Inventory" Start failed: ...
If a cleanup task errors, Clean reports:
Rivet Clean cleanup failed: ...
Those messages intentionally point at the managed Unit or cleanup task path instead of leaving you with only a raw callback error.
Next: Surfaces.