Forest Logo
search
package_2

loom

By @mrkirdid

Roblox

Mirrored from Wally

Loom

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 Init runs one at a time, lowest Priority first, with ties broken by Name. Then every Start runs in its own thread, in the same order. A unit's Init can rely on every earlier unit's Init having finished.
  • Prepare / Activate work as other names for Init / Start.
  • Enabled = false skips a unit. If Priority is set to anything other than a number, the unit fails to load.
  • Strict tables (an __index that throws on a missing key) are fine. Inherited methods are fine too.
  • Name defaults to the module's name. Two units with the same Name is an error, and the one whose full path sorts later loses.
  • Units should require each other directly. Loom:Get(Name) is for code that lives outside the unit folders, such as Cmdr commands.

Config

KeySideDefault
Servicesserver—Folder or list of Folders
Controllersbothfrom serverFolder or list of Folders; must be visible to the client
Preloadclientofftrue or a table, see below
Excludeboth{}Unit names to skip
SlowWarningboth5Seconds before a running require/Init is reported
DebugbothfalsePrints 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, Init or Start throws). Loom warns with the error and its stack trace, records it in Loom:GetFailures(), and fires Loom.Failed. Every other unit carries on, and a unit whose Init failed never gets Start.
  • A server service fails. Clients still start, and they warn which services failed.
  • An Init or require is slow. After SlowWarning seconds 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 the require line it is stuck on.

Preload (client)

Loom:Init({
	Preload = {
		Critical = { Assets.Animations },
		Background = { RenderTree() },
		Timeout = 15,
	},
})
  • Critical holds back controller Starts until it has loaded or Timeout (default 15s) runs out. Background loads after it and holds nothing back.
  • Lists take Instances (searched with all their descendants) and content-id strings.
  • Preload = true is 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 instance PreloadIgnore (or set IgnoreTag) 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 at Total/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

  1. Unithub = "mrkirdid/unithub@..." → Loom = "mrkirdid/loom@^1.0.0", then wally install.
  2. require(...Unithub) → require(...Loom), and Unithub:init / Unithub:Init → Loom:Init.
  3. Config keys are PascalCase only: services → Services, controllers → Controllers, ExcludedUnits → Exclude. Timeout, LifecycleTimeout and Watch no longer exist.
  4. Server-side Preload moves into the client's Loom:Init. In the preload config, CriticalRoots → Critical, BackgroundRoots → Background, CriticalTimeout → Timeout, and Aggressive = true becomes Preload = true.
  5. GetService / GetController / Get → Get. WaitFor* → require the module, or Loom: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 the Inits rather than the longest one. A service that does heavy work in Init (CrazyGoose's MapService) now holds up everything with a higher Priority. Give it a high Priority, or move the work to Start.
  • An Init that waits for something a later unit creates in its own Init will now hang. The slow warning shows the line it is stuck on. Fix it by lowering the creating unit's Priority.
  • There are no timeouts. CrazyGoose's LifecycleTimeout = 60 can be deleted.
  • Clients are no longer held back by a failed service. The workarounds in CrazyGoose's MeshKit and ValeKit (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

check_circle

Safe for commercial use

Automated license review — not legal advice.