{"id":"mrkirdid/loom","name":"loom","scope":"mrkirdid","platform":"roblox","description":"Service/controller loader: Init in Priority order, isolated failures, traced slow hooks, and a client preloader. No dependencies.","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"6d691d47f9dae6efc2c15e28fc7103ba894c58062a9a3ccba9addfe27a1943d6","likes":0,"downloads":0,"install":"forest install mrkirdid/loom","url":"https://forest.dev/p/roblox/mrkirdid/loom","files":"https://api.forest.dev/ai/package/roblox/mrkirdid/loom/files","readme":"# Loom\n\nLoads 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.\n\n```toml\n[dependencies]\nLoom = \"mrkirdid/loom@^1.0.0\"\n```\n\n## Bootstrap\n\n```luau\n-- server\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Shared = ReplicatedStorage.Shared\n\nlocal Loom = require(Shared.Packages.Loom)\n\nLoom:Init({\n\tServices = script.Services,\n\tControllers = Shared.Controllers,\n})\n```\n\n```luau\n-- client\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Shared = ReplicatedStorage:WaitForChild(\"Shared\")\n\nlocal Loom = require(Shared.Packages.Loom)\n\nLoom:Init()\n```\n\nThe client gets its controller folders from the server, so `Loom:Init()` needs no arguments. `Init` yields until every `Start` has been called.\n\n## Units\n\nA 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.\n\n```luau\nlocal Inventory = {\n\tName = \"Inventory\",\n\tPriority = 10,\n}\n\nfunction Inventory:Init()\n\t-- bind: connections, remotes, input\nend\n\nfunction Inventory:Start()\n\t-- run: loops, spawns\nend\n\nreturn Inventory\n```\n\n- **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.\n- `Prepare` / `Activate` work as other names for `Init` / `Start`.\n- `Enabled = false` skips a unit. If `Priority` is set to anything other than a number, the unit fails to load.\n- Strict tables (an `__index` that throws on a missing key) are fine. Inherited methods are fine too.\n- `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.\n- Units should `require` each other directly. `Loom:Get(Name)` is for code that lives outside the unit folders, such as Cmdr commands.\n\n## Config\n\n| Key | Side | Default | |\n|---|---|---|---|\n| `Services` | server | — | Folder or list of Folders |\n| `Controllers` | both | from server | Folder or list of Folders; must be visible to the client |\n| `Preload` | client | off | `true` or a table, see below |\n| `Exclude` | both | `{}` | Unit names to skip |\n| `SlowWarning` | both | `5` | Seconds before a running require/`Init` is reported |\n| `Debug` | both | `false` | Prints timings and the slowest `Init`s |\n\nIf 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?`.\n\n## When things go wrong\n\n- **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`.\n- **A server service fails.** Clients still start, and they warn which services failed.\n- **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.\n- **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.`\n- **Loom itself crashes on the server.** The server publishes the error and re-raises it. Clients then error with it instead of waiting forever.\n- **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.\n\n## Preload (client)\n\n```luau\nLoom:Init({\n\tPreload = {\n\t\tCritical = { Assets.Animations },\n\t\tBackground = { RenderTree() },\n\t\tTimeout = 15,\n\t},\n})\n```\n\n- `Critical` holds back controller `Start`s until it has loaded or `Timeout` (default 15s) runs out. `Background` loads after it and holds nothing back.\n- Lists take Instances (searched with all their descendants) and content-id strings.\n- `Preload = true` is shorthand for Critical = StarterGui + ReplicatedFirst, Background = ReplicatedStorage + Workspace.\n- 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.\n- Preloading starts as soon as the game has loaded and overlaps the wait for the server.\n- `Loom.PreloadProgress:Connect(function(Loaded, Total) end)` drives a loading bar, and it always finishes at `Total/Total`.\n\n## API\n\n```luau\nLoom:Init(Config?)\nLoom:Get(Name): Unit          -- errors if the unit is unknown, excluded or failed in Init\nLoom:AwaitStart()             -- yields until Init has finished, returns at once after\nLoom:IsStarted(): boolean\nLoom:GetFailures(): { Failure } -- { Name, Phase: \"Require\" | \"Init\" | \"Start\", Error, Module }\n\nLoom.Started                  -- Signal<>\nLoom.Failed                   -- Signal<Failure>\nLoom.PreloadProgress          -- Signal<number, number>\n```\n\n## Moving from Unithub\n\n1. `Unithub = \"mrkirdid/unithub@...\"` → `Loom = \"mrkirdid/loom@^1.0.0\"`, then `wally install`.\n2. `require(...Unithub)` → `require(...Loom)`, and `Unithub:init` / `Unithub:Init` → `Loom:Init`.\n3. Config keys are PascalCase only: `services` → `Services`, `controllers` → `Controllers`, `ExcludedUnits` → `Exclude`. `Timeout`, `LifecycleTimeout` and `Watch` no longer exist.\n4. 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`.\n5. `GetService` / `GetController` / `Get` → `Get`. `WaitFor*` → require the module, or `Loom:AwaitStart()`.\n\nBehaviour changes to check:\n\n- **`Init`s used to all run at once; now they run one after another.** Startup time is now the sum of the `Init`s 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`.\n- **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`.\n- **There are no timeouts.** CrazyGoose's `LifecycleTimeout = 60` can be deleted.\n- **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.\n- **The Parallel dispatcher, proxies, runtime folder watching and camelCase aliases are gone.**\n\n## Developing\n\n`test.project.json` builds a test place that exercises ordering, every kind of failure, slow-`Init` traces, the server wait and preload:\n\n```bash\nrojo build test.project.json -o LoomTest.rbxlx\n```\n\nOpen 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.\n","readmeTruncated":false}