Skip to content

Instantly share code, notes, and snippets.

@Mark-Marks
Created December 18, 2025 09:56
Show Gist options
  • Select an option

  • Save Mark-Marks/bd3812fd5ead9abf11cb707ab7467cfd to your computer and use it in GitHub Desktop.

Select an option

Save Mark-Marks/bd3812fd5ead9abf11cb707ab7467cfd to your computer and use it in GitHub Desktop.
Roblox module loader with topological sorting via dependency resolution
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,
}
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