Start typing to search packages!
loom
By @mrkirdid
Roblox
Mirrored from WallyLoom
Loads your services and controllers, runs Init in Priority order, then Start. When a unit breaks, Loom says exactly where and the rest of the game keeps going. Optional client preloading runs while the client waits for the server. No dependencies.
[dependencies]
Loom = "mrkirdid/loom@^1.0.0"
Bootstrap
-- server
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Shared = ReplicatedStorage.Shared
local Loom = require(Shared.Packages.Loom)
Loom:Init({
Services = script.Services,
Controllers = Shared.Controllers,
})
-- client
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Shared = ReplicatedStorage:WaitForChild("Shared")
local Loom = require(Shared.Packages.Loom)
Loom:Init()
The client gets its controller folders from the server, so Loom:Init() needs no arguments. Init yields until every Start has been called.
Units
A unit is any ModuleScript whose direct parent is a Folder, anywhere under the folders you pass in, that isn't inside another unit. A unit can keep helper modules as its children, or in folders under it, and they are never loaded as units.
local Inventory = {
Name = "Inventory",
Priority = 10,
}
function Inventory:Init()
-- bind: connections, remotes, input
end
function Inventory:Start()
-- run: loops, spawns
end
return Inventory
- Order. Every unit is required (in parallel). Then each
Initruns one at a time, lowestPriorityfirst, with ties broken byName. Then everyStartruns in its own thread, in the same order. A unit'sInitcan rely on every earlier unit'sInithaving finished. Prepare/Activatework as other names forInit/Start.Enabled = falseskips a unit. IfPriorityis set to anything other than a number, the unit fails to load.- Strict tables (an
__indexthat throws on a missing key) are fine. Inherited methods are fine too. Namedefaults to the module's name. Two units with the sameNameis an error, and the one whose full path sorts later loses.- Units should
requireeach other directly.Loom:Get(Name)is for code that lives outside the unit folders, such as Cmdr commands.
Config
| Key | Side | Default | |
|---|---|---|---|
Services | server | — | Folder or list of Folders |
Controllers | both | from server | Folder or list of Folders; must be visible to the client |
Preload | client | off | true or a table, see below |
Exclude | both | {} | Unit names to skip |
SlowWarning | both | 5 | Seconds before a running require/Init is reported |
Debug | both | false | Prints timings and the slowest Inits |
If a key is unknown, Loom errors and lists the keys it accepts. A key with the wrong case gets a hint, e.g. "services" → Did you mean Services?.
When things go wrong
- A unit fails (require error, returns a non-table, duplicate name,
InitorStartthrows). Loom warns with the error and its stack trace, records it inLoom:GetFailures(), and firesLoom.Failed. Every other unit carries on, and a unit whoseInitfailed never getsStart. - A server service fails. Clients still start, and they warn which services failed.
- An
Initor require is slow. AfterSlowWarningseconds Loom warns with the stack of the line it is yielding on, e.g.Beta:Init is still running after 5s. It is yielding here: ...Beta:10. It is never killed. A timeout would leave a half-initialised unit behind, so instead Loom shows you where the wait is. - The client is waiting on the server. After 10 seconds the client warns what the server is doing, e.g.
Still waiting for server services after 10s. Server is at: DataService:Init. - Loom itself crashes on the server. The server publishes the error and re-raises it. Clients then error with it instead of waiting forever.
- Circular requires. If a module yields (for example on
WaitForChild) before it requires something that requires it back, Roblox never detects the loop and both requires hang. This is engine behaviour, and requiring one module at a time doesn't avoid it. The slow-require warning points at therequireline it is stuck on.
Preload (client)
Loom:Init({
Preload = {
Critical = { Assets.Animations },
Background = { RenderTree() },
Timeout = 15,
},
})
Criticalholds back controllerStarts until it has loaded orTimeout(default 15s) runs out.Backgroundloads after it and holds nothing back.- Lists take Instances (searched with all their descendants) and content-id strings.
Preload = trueis shorthand for Critical = StarterGui + ReplicatedFirst, Background = ReplicatedStorage + Workspace.- Each content id is requested once, however many instances use it, and built-in
rbxasset://content is skipped. Tag an instancePreloadIgnore(or setIgnoreTag) to skip it and everything under it. - Preloading starts as soon as the game has loaded and overlaps the wait for the server.
Loom.PreloadProgress:Connect(function(Loaded, Total) end)drives a loading bar, and it always finishes atTotal/Total.
API
Loom:Init(Config?)
Loom:Get(Name): Unit -- errors if the unit is unknown, excluded or failed in Init
Loom:AwaitStart() -- yields until Init has finished, returns at once after
Loom:IsStarted(): boolean
Loom:GetFailures(): { Failure } -- { Name, Phase: "Require" | "Init" | "Start", Error, Module }
Loom.Started -- Signal<>
Loom.Failed -- Signal<Failure>
Loom.PreloadProgress -- Signal<number, number>
Moving from Unithub
Unithub = "mrkirdid/unithub@..."→Loom = "mrkirdid/loom@^1.0.0", thenwally install.require(...Unithub)→require(...Loom), andUnithub:init/Unithub:Init→Loom:Init.- Config keys are PascalCase only:
services→Services,controllers→Controllers,ExcludedUnits→Exclude.Timeout,LifecycleTimeoutandWatchno longer exist. - Server-side
Preloadmoves into the client'sLoom:Init. In the preload config,CriticalRoots→Critical,BackgroundRoots→Background,CriticalTimeout→Timeout, andAggressive = truebecomesPreload = true. GetService/GetController/Get→Get.WaitFor*→ require the module, orLoom:AwaitStart().
Behaviour changes to check:
Inits used to all run at once; now they run one after another. Startup time is now the sum of theInits rather than the longest one. A service that does heavy work inInit(CrazyGoose's MapService) now holds up everything with a higherPriority. Give it a highPriority, or move the work toStart.- An
Initthat waits for something a later unit creates in its ownInitwill now hang. The slow warning shows the line it is stuck on. Fix it by lowering the creating unit'sPriority. - There are no timeouts. CrazyGoose's
LifecycleTimeout = 60can be deleted. - Clients are no longer held back by a failed service. The workarounds in CrazyGoose's
MeshKitandValeKit(return nil instead of throwing) are no longer needed. - The Parallel dispatcher, proxies, runtime folder watching and camelCase aliases are gone.
Developing
test.project.json builds a test place that exercises ordering, every kind of failure, slow-Init traces, the server wait and preload:
rojo build test.project.json -o LoomTest.rbxlx
Open it in Studio and press Play. The output should end with [LoomTest] server: 23 passed, 0 failed and [LoomTest] client: 5 passed, 0 failed. Set a BetaWait attribute on Workspace above 10 to see the client's "still waiting" warning.
Package Details
Install command (Click to copy)
Version
1.0.0
License
MIT
Safe for commercial use
Automated license review — not legal advice.
