Roam
Roam is a systems bootstrapping tool for Roblox luau projects, designed to make it easy to initialize and start services in a topologically sorted manner without the need to manually order and start services.
Roam follows a design pattern similar to Knit, but is more lightweight. It removes all networking and replication functionality, and instead focuses on providing a simple methodology to easily initialize and start Services given to it.
Roam is RunContext agnostic, meaning it can be used on both the server and client in the same manner. It makes no distinction between the two, and instead focuses on providing a simple interface for initializing and starting services. This means you could create a service and register it on both the server and client, and it will be initialized and started on both ends.
[EXAMPLE SERVICE]
-- MyService.lua
local MyService = {}
function MyService:RoamInit()
print("MyService initialized!")
end
function MyService:RoamStart()
print("MyService started!")
end
-- Register the service table with Roam
local Roam = require(ReplicatedStorage.Roam)
Roam.registerService(MyService, "MyService")
return MyService
[EXAMPLE STARTUP]
-- ServerBootstrapper.server.lua
local Roam = require(ReplicatedStorage.Roam)
-- Require your services. (Tip: Roam.requireModules can help abstract this process!)
require(ReplicatedStorage.MyService)
-- Start Roam
Roam.start()
:andThenCall(print, "Roam started!")
:catch(warn)
[CONTRACTS]
- Services must be created/registered before Roam is started.
- Services must be created/registered with a unique name.
-
Services with
RoamInitandRoamStartmethods will have those methods called when Roam is started at the appropriate time. (Names are configurable) RequiredServicesboot in proper topological order if specified in the ServiceConfig.- Roam functions the same regardless of RunContext (Server/Client).
Setting up Services
Services can be set up in a variety of ways. The most common way is to create a ModuleScript that returns a table with the methods you want to define, and then register it with Roam just prior to the final module's return.
See Roam.registerService for more information on setting up a new service.
Networking
Roam does not inherently have networking functionality. However, it can easily be added through the use of NetWire's .setupServiceNetworking function.
LIFECYCLE
For those interested in the full execution order roam follows for booting services, see the diagrams below:
Initialization Phase (Synchronous):
GlobalPreInit- Called once before ANY service initializes (global setup)- For each service (in dependency order):
- Await dependencies to finish initializing
PreInit(service)- Called before this service's RoamInit- Service's
RoamInit()method executes PostInit(service)- Called after this service's RoamInit
GlobalPostInit- Called once after ALL services finish initializing
Start Phase (Fully Async - Does Not Block):
GlobalPreStart- Called once before ANY service starts.- For each service (in dependency order):
PreStart(service)- Called before this service's RoamStart- Service's
RoamStart()method executes PostStart(service)- Called after this service's RoamStart
GlobalPostStart- Called once after ALL services start
Key Concepts:
- Parallel Initialization: Services without dependencies initialize concurrently via coroutines
- Dependency Blocking: Services wait for their dependencies' PostInit before starting their PreInit
- Synchronous Init Phase: The Initialization Phase completes fully before Start Phase begins
- Async Start Phase: The Start Phase spawns services lifecycle methods asynchronously
Types
Service
type Service = tableA service is a table that can be registered with Roam.
ServiceConfig
interface ServiceConfig {Name: string--
Name of the Service. Must be unique. Used when accessing via .getService
RequiredServices: {Service}?--
The Services that this Service depends on. Roam will ensure that these Services are initialized before this Service.
StartMethodName: string?--
Overrides default StartMethodName of "RoamStart"
InitMethodName: string?--
Overrides default InitMethodName of "RoamInit"
}local myOtherService = require(ReplicatedStorage.MyOtherService)
-------------------------------------------------
local MyService = {}
function MyService:CustomStartMethod()
print("MyService started!")
end
-------------------------------------------------
Roam.registerService(MyService, {
Name = "MyService",
RequiredServices = {myOtherService},
StartMethodName = "CustomStartMethod",
})
return MyService
Deferring RequiredServices
Do NOT add services to the RequiredServices after you have created or registered the service. This will cause undefined behavior.
StartConfig
interface StartConfig {GlobalPreInit: (() → ())?--
Called once before ANY service initializes
GlobalPostInit: (() → ())?--
Called once after ALL services finish initializing
GlobalPreStart: (() → ())?--
Called once before ANY service starts. (Async)
GlobalPostStart: (() → ())?--
Called once after ALL services start (Async)
}Yielding in Init lifecycle hooks will prevent Roam from progressing to the next step.
Start lifecycle hooks are fully asynchronous and do not block progression at any point.
Properties
Bootstrappers
A table of generic bootstrappers for Roam that you can use to quickly setup new projects.
Roam.Bootstrappers.Server(script)
:andThenCall(print, "Roam Server Bootstrapped!")
Functions
registerService
Registers a Service/Table with Roam to be Initialized and Started when Roam starts. Cannot be called after Roam has been started.
local MyService = {}
function MyService:RoamInit()
print("MyService initialized!")
end
function MyService:RoamStart()
print("MyService started!")
end
----------------------------------------------------------------
Roam.registerService(MyService, "MyService")
start
Starts Roam. Should only be called once. Calling multiple times will result in a promise rejection.
Optional config argument provides lifecycle hooks.
Roam.start({
GlobalPreInit = function()
print("=== Initialization Phase Starting ===")
end,
PreInit = function(service)
print("Initializing:", Roam.getServiceName(service))
end,
PostInit = function(service)
print("✓ Initialized:", Roam.getServiceName(service))
end,
GlobalPostInit = function()
print("=== All Services Initialized ===")
end,
})
:andThenCall(print, "Roam started!")
:catch(warn)
CAUTION
Be sure that all services have been created before
calling Start. Services cannot be added later.
onStart
Roam.onStart() → Promise
Returns a promise that is resolved once Roam has started. This is useful
for any code that needs to tie into Roam services but is not the script
that called start.
Roam.onStart():andThen(function()
local MyService = require(ReplicatedStorage.MyService)
MyService:DoSomething()
end):catch(warn)
isReady
Roam.isReady() → booleanReturns whether or not Roam has been successfully started and is ready for external access.
requireModules
Roam.requireModules(config: {DeepSearch: boolean?,AllowYieldingRequires: boolean?,StopOnFailedRequire: boolean?,}?) → {Success: boolean,}Requires all the modules that are children of the given parent. This is an easy way to quickly load all services that might be in a folder. Takes an optional predicate function to filter which modules are loaded. Services collected this way must not yield.
DeepSearch-> whether it checks descendants or just childrenAllowYieldingRequires-> whether to allow required modules to yield (default: false)RequirePredicate-> a predicate function that determines whether a module should be requiredIgnoreDescendantsPredicate-> A Predicate for whether the Descendants of an instance should be Searched (Only matters if DeepSearch is true)StopOnFailedRequire-> whether to stop requiring modules if one fails to require (default: false). Useful for debugging, helps to clear excessive noise from the output.
local pred = function(obj: ModuleScript): boolean
return obj.Name:match("Service$") ~= nil
end
Roam.requireModules(ReplicatedStorage.Shared, {
DeepSearch = true,
RequirePredicate = pred,
IgnoreDescendantsPredicate = function(obj: Instance): boolean
return obj.Name == "Ignore"
end,
})