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().
Mental model
Section titled “Mental model”What a scope accepts
Section titled “What a scope accepts”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.
Order and ownership
Section titled “Order and ownership”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 scopeDestroyed, 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 underxpcall; a failure logs[Scope] cleanup failed:with the traceback and the rest still run.- A second
destroy()does nothing. A cleanup that callsdestroy()again returns at once. - A resource added to a destroyed scope is cleaned immediately and returned;
Countstays 0.
Nested scopes
Section titled “Nested scopes”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.
Tweens
Section titled “Tweens”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-owned scopes
Section titled “Kernel-owned scopes”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:
--!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 = kernelend
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 endend
function ArenaService.stop(self: any) if self.Lifetime then self.Lifetime:destroy() -- closes the current round scope too endend
return ArenaServiceExplicit 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 nameslocal Blink = Lifetime:add(kernel:spawnProcess(function(part: BasePart) while true do part.Transparency = 1 - part.Transparency Process.sleep(0.5) endend, { Name = "Blink" }), "kill")Blink:start(workspace.Beacon)
-- A raw thread needs its own callbacklocal Watcher = task.spawn(function() local Gate = workspace:WaitForChild("ArenaGate", 30) if Gate then kernel.Bus:publish("Arena.GateFound", Gate) endend)Lifetime:add(function() if coroutine.status(Watcher) == "suspended" then task.cancel(Watcher) endend)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 subscriptionend))API reference
Section titled “API reference”| 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. |