Created
December 18, 2025 09:56
-
-
Save Mark-Marks/bd3812fd5ead9abf11cb707ab7467cfd to your computer and use it in GitHub Desktop.
Roblox module loader with topological sorting via dependency resolution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type Array<T> = { T } | |
| type Map<K, V> = { [K]: V } | |
| type Graph<T> = Map<T, Array<T>> | |
| -- Based on Kahn's algorithm | |
| local function toposort<T>(graph: Graph<T>): Array<T> | |
| local in_degree: Map<T, number> = {} | |
| for node, neighbors in graph do | |
| in_degree[node] = 0 | |
| for _, neighbor in neighbors do | |
| if not in_degree[neighbor] then | |
| in_degree[neighbor] = 0 | |
| end | |
| (in_degree :: any)[neighbor] += 1 | |
| end | |
| end | |
| local queue: Array<T> = {} | |
| for node, degree in in_degree do | |
| if degree == 0 then | |
| table.insert(queue, node) | |
| end | |
| end | |
| local sorted_order: Array<T> = {} | |
| while #queue > 0 do | |
| local node: T = table.remove(queue, 1) :: any | |
| table.insert(sorted_order, node) | |
| for _, neighbor in (graph[node] or {}) :: { T } do | |
| local degree = in_degree[neighbor] - 1 | |
| in_degree[neighbor] = degree | |
| if degree == 0 then | |
| table.insert(queue, neighbor) | |
| end | |
| end | |
| end | |
| return sorted_order | |
| end | |
| local function get_modules(parent: Instance): { ModuleScript } | |
| local modules = {} | |
| for _, child in parent:GetChildren() do | |
| if child:IsA("ModuleScript") then | |
| table.insert(modules, child) | |
| end | |
| end | |
| return modules | |
| end | |
| type Singleton = { [any]: any } | |
| --- Loads all modulescripts under the given instance. | |
| --- Topologically sorts the init & start order by dependencies. | |
| local function load(container: Instance) | |
| local start = os.clock() | |
| local graph: Graph<Singleton> = {} | |
| local to_inject: Map<Singleton, Map<string, Singleton>> = {} | |
| local name_lookup: Map<Singleton, string> = {} | |
| for _, module in get_modules(container) do | |
| local singleton = (require)(module) | |
| local dependencies = {} | |
| local uninitialized_dependencies: Map<string, Singleton> = {} | |
| for name, value in singleton do | |
| if type(value) == "table" and value.UNINITIALIZED_DEPENDENCY == true then | |
| table.insert(dependencies, value.singleton) | |
| uninitialized_dependencies[name] = value.singleton | |
| end | |
| end | |
| graph[singleton] = dependencies | |
| to_inject[singleton] = uninitialized_dependencies | |
| name_lookup[singleton] = module.Name | |
| end | |
| local ordered = toposort(graph) | |
| table.clear(graph) | |
| for idx = #ordered, 1, -1 do | |
| local singleton = ordered[idx] | |
| for name, dependency in to_inject[singleton] do | |
| singleton[name] = dependency | |
| end | |
| if singleton.init then | |
| -- Don't allow for yielding | |
| -- selene: allow(empty_loop) | |
| for _ in | |
| function() | |
| (singleton :: any):init() | |
| end :: any | |
| do | |
| end | |
| end | |
| end | |
| for idx = #ordered, 1, -1 do | |
| local singleton = ordered[idx] | |
| if singleton.start then | |
| task.spawn(singleton.start :: any, singleton) | |
| end | |
| end | |
| local took = math.round((os.clock() - start) * 1000) | |
| print(`✅ Loaded {#ordered} singletons, took {took}ms`) | |
| end | |
| --- Requires all modulescripts under the given instance with no extra logic. | |
| --- Useful for modules (eg. ECS systems) which have their own scheduling logic. | |
| local function noop(container: Instance) | |
| local start = os.clock() | |
| local modules = get_modules(container) | |
| for _, module in modules do | |
| (require)(module) | |
| end | |
| local took = math.round((os.clock() - start) * 1000) | |
| print(`✅ Loaded {#modules} systems, took {took}ms`) | |
| end | |
| --- Marks the given singleton as a dependecy to resolve when loading. | |
| local function use<T>(singleton: T): T | |
| return { UNINITIALIZED_DEPENDENCY = true, singleton = singleton } :: any | |
| end | |
| return { | |
| load = load, | |
| noop = noop, | |
| use = use, | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local Singleton = {} | |
| Singleton.dependency = loader.use(require("@dependency")) | |
| -- Called after dependency resolution | |
| function Singleton.init(self: self) | |
| self.value = 53 | |
| -- `dependency` was init'd prior to this singleton | |
| self.requires_dependency = self.dependency:get_value() | |
| end | |
| -- `task.spawn`'d after all singletons are init'd | |
| function Singleton.start(self: self) | |
| -- `dependency` was started prior to this singleton | |
| end | |
| export type self = typeof(Singleton) | |
| return Singleton |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment