Skip to content

Scope

Owns connections, timers, instances, tweens and cleanup callbacks under one lifetime and releases them in reverse order when it closes.

Realm
Shared
Require
ReplicatedStorage.ChloeKernel.Scope
Since
v0.8.0
Source
Scope.luau

A scope gives related resources one lifetime. A service, a UI panel or a live entity adds its connections, timer handles, instances, tweens and cleanup functions to a scope, and destroy() releases all of them in reverse registration order. Ownership is explicit and cheap: adding and removing a resource is O(1), closing visits only what the scope still owns, and a resource added after the scope closed is released at once instead of leaking. kernel:createScope() hands out scopes owned by the kernel, which closes any still open at shutdown. Per-player lifetimes use sessions, which also accept a scope as a binding; timer handles come from the Scheduler; and a service closes its scope in stop().

scope:add(resource, method?) picks the cleanup when the resource is added and keeps it:

Resource Cleanup
A function Called with no arguments. Passing a method errors cleanup functions do not take a method name.
An Instance :Destroy().
An RBXScriptConnection :Disconnect().
A table The first method found among destroy, Destroy, disconnect, Disconnect, cancel and Cancel, called on the table.
A table or Instance with method resource[method](resource). Errors scope cleanup method is not a function if that field is not a function.

That covers Signal and Bus connections (disconnect), Scheduler handles (cancel), other scopes (destroy), tweens (Cancel) and most Roblox-style objects. Anything else errors unsupported scope resource, a table with none of the methods errors scope resource requires a cleanup method, and adding a scope to itself errors a scope cannot own itself. A process has only kill, so it needs the explicit form: scope:add(process, "kill"). A raw thread needs a function that cancels it.

A scope keeps its entries in a doubly linked list in registration order, plus a map from resource to entry:

  • Adding a resource the scope already owns returns it without a second entry.
  • remove(resource) unlinks the entry without cleaning it and returns whether it was owned. Cleanup is now the caller’s job. The other entries keep their order.
  • destroy() marks the scope Destroyed, detaches it from its parent, then walks from the newest entry to the oldest. Each entry is unlinked before its cleanup runs, so a cleanup that touches the scope sees a consistent list. Every cleanup runs under xpcall; a failure logs [Scope] cleanup failed: with the traceback and the rest still run.
  • A second destroy() does nothing. A cleanup that calls destroy() again returns at once.
  • A resource added to a destroyed scope is cleaned immediately and returned; Count stays 0.

scope:child() creates a scope owned by this one. Destroying the child on its own also removes it from the parent, so a long-lived parent does not keep closed children around. Destroying the parent destroys its children, newest first, and their descendants. child() on a destroyed scope returns a scope that is already destroyed.

scope:tween(instance, tweenInfo, goals) creates the tween with TweenService:Create, adds it with Cancel as its cleanup, plays it and returns it. A tween that completes leaves the scope by itself, so repeated tweens on a long-lived scope do not pile up. Destroying the scope calls Tween:Cancel(), which stops playback and leaves the properties where they are; it does not reset them. A destroyed scope returns the tween without playing it. Added in 0.9.0.

kernel:createScope() creates the kernel’s root scope on first use and returns a child of it. Closing that child early removes it from the root, so a scope per UI view or per round leaves nothing behind in the kernel.

kernel:shutdown() destroys the root after the service stop phase and before it kills processes and destroys the scheduler, the Bus and the hook registry. A scope cleanup can therefore still cancel scheduler handles and disconnect Bus subscriptions. Once shutdown begins, createScope() asserts kernel is shut down on the core and server kernel is shut down on the server kernel.

A live server close runs only the service stop phase and never calls shutdown(). Kernel scopes are not destroyed on close, so a scope whose cleanup must run at close belongs to a service that destroys it in stop(). Kernel shutdown is only a backstop.

The restricted kernel facade exposes dot-style createScope() and after(delaySeconds, fn, priority?), so userspace code can create scopes and the timers they own. On the server facade createScope goes through the server kernel and is rejected as soon as server shutdown begins.

A standalone scope from Scope.new() belongs to no kernel. Whoever creates it destroys it.

A service with one scope for its whole life and a child scope per round:

src/Server/Game/Services/ArenaService.luau
--!strict
-- ArenaService: opens the arena for each round and cleans up after it.
local Workspace = game:GetService("Workspace")
local ArenaService = {
Name = "ArenaService",
}
function ArenaService.init(self: any, kernel: any)
self.Kernel = kernel
end
function ArenaService.start(self: any)
self.Lifetime = self.Kernel:createScope()
self.Lifetime:add(self.Kernel.Bus:subscribe("Round.Started", function()
self:beginRound()
end))
self.Lifetime:add(self.Kernel.Bus:subscribe("Round.Ended", function()
self:endRound()
end))
end
function ArenaService.beginRound(self: any)
self:endRound()
local Round = self.Lifetime:child()
self.Round = Round
local Doors = Workspace:FindFirstChild("ArenaDoors")
if Doors then
for _, Door in Doors:GetChildren() do
if Door:IsA("BasePart") then
Round:tween(Door, TweenInfo.new(1), { Transparency = 1 })
end
end
end
-- Sudden death after two minutes unless the round ends first
Round:add(self.Kernel.Scheduler:after(120, function()
self.Kernel.Bus:publish("Arena.SuddenDeath")
end))
local Hazard = Instance.new("Part")
Hazard.Name = "RoundHazard"
Hazard.Anchored = true
Hazard.Parent = Workspace
Round:add(Hazard)
end
function ArenaService.endRound(self: any)
if self.Round then
-- Cancels the timer and any door tween still playing, destroys the hazard, leaves the parent
self.Round:destroy()
self.Round = nil
end
end
function ArenaService.stop(self: any)
if self.Lifetime then
self.Lifetime:destroy() -- closes the current round scope too
end
end
return ArenaService

Explicit cleanup for resources without a standard method name:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Process = require(ReplicatedStorage.ChloeKernel.Process)
local Lifetime = kernel:createScope()
-- A process has kill() and none of the default method names
local Blink = Lifetime:add(kernel:spawnProcess(function(part: BasePart)
while true do
part.Transparency = 1 - part.Transparency
Process.sleep(0.5)
end
end, { Name = "Blink" }), "kill")
Blink:start(workspace.Beacon)
-- A raw thread needs its own callback
local Watcher = task.spawn(function()
local Gate = workspace:WaitForChild("ArenaGate", 30)
if Gate then
kernel.Bus:publish("Arena.GateFound", Gate)
end
end)
Lifetime:add(function()
if coroutine.status(Watcher) == "suspended" then
task.cancel(Watcher)
end
end)

Userspace code through the restricted facade:

local ServerKernel = require(game:GetService("ServerScriptService").ChloeKernelServer)
local Api = ServerKernel.current():api()
local Countdown = Api.createScope()
Countdown:add(Api.after(10, function()
Api.publish("Lobby.Start")
end))
Countdown:add(Api.subscribe("Lobby.Cancelled", function()
Countdown:destroy() -- cancels the pending start and this subscription
end))
Member Description
kernel:createScope(): Scope A child of the kernel’s root scope. Closed by kernel:shutdown() if nothing closed it first.
Scope.new(): Scope A standalone scope owned by no kernel. require(ReplicatedStorage.ChloeKernel.Scope).
scope:add(resource, method?): resource Takes ownership and returns the resource. On a destroyed scope, cleans it at once.
scope:remove(resource): boolean Gives up ownership without cleaning. false if the scope did not own it.
scope:tween(instance, tweenInfo, goals): Tween Creates, owns and plays a tween. It leaves the scope when it completes and is cancelled if the scope closes first.
scope:child(): Scope A nested scope, closed with this one or on its own.
scope:destroy() Cleans every owned resource, newest first. Idempotent.
scope.Destroyed true once destroy() began.
scope.Count Number of resources currently owned.
scope.Parent The owning scope, or nil for a root, a standalone scope or a child that closed.
api.createScope() The restricted facade’s createScope.
api.after(delaySeconds, fn, priority?) The restricted facade’s Scheduler:after. The returned handle can be added to a scope.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.