Services
Services are your game’s systems: plain tables registered on the kernel before start(), booted in dependency order, stopped in reverse. The defining design decision is the two-phase boot — every service’s init runs before any service’s start — so by the time any start runs, every dependency is fully initialized and safe to use. There is no lazy loading, no proxy objects, no partially-constructed state to reason about.
Mental model
Section titled “Mental model”The ServiceManager is a registry plus a topological sort.
- Registration window. Between
boot()andstart()(in practice: inside yourBootstrap— see Kernel & Boot),kernel:registerService(definition)records definitions by name. Names must be unique; registration after boot is an error. Nothing runs yet. - Order resolution.
kernel:start()resolves the boot order with a depth-first walk overDependencies. Root names are visited in alphabetical order, so the order is deterministic across runs — two independent services always boot in the same relative order. The walk tracks its active path, so a cycle errors with the full chain (service dependency cycle: A -> B -> C -> A), not just the closing edge. - Init phase. Every
init(self, kernel)runs in boot order. This is where a service builds its own state. - Start phase. Every
start(self)runs in the same order. This is where a service reaches for other services —getServiceon any dependency is guaranteed to return a fully-initialized table, because all inits already ran. - Shutdown. The kernel’s
shutdown()(kernel.Core:shutdown()on the server) callsstop(self)on each service in reverse boot order, so a service’s dependencies outlive it.
Both init and start are wrapped in xpcall with debug.traceback: a throwing service aborts the boot with the service named and the traceback attached. Boot failures are loud by design — a game half-booted is worse than a game that refuses to boot.
-- src/Server/Bootstrap.luaureturn function(kernel) kernel:registerService({ Name = "Inventory", init = function(self, booted) self.Kernel = booted self.Items = {} end, })
kernel:registerService({ Name = "Quests", Dependencies = { "Inventory" }, init = function(self, booted) self.Kernel = booted self.Active = {} end, start = function(self) -- guaranteed initialized: Inventory's init already ran self.Inventory = self.Kernel:getService("Inventory") end, stop = function(self) -- reverse boot order: runs before Inventory's stop table.clear(self.Active) end, })endRegistration order does not matter — Quests may register before Inventory. Only Dependencies orders the boot.
A service is just its definition table, so any extra fields you put on it are the service’s public surface:
kernel:registerService({ Name = "Wallet", init = function(self) self.Balances = {} end, -- extra members become the service API getBalance = function(self, player: Player): number return self.Balances[player] or 0 end,})
-- later, anywhere after boot:local Wallet = Kernel:getService("Wallet")print(Wallet:getBalance(SomePlayer))The service definition
Section titled “The service definition”export type ServiceDefinition = { Name: string, Dependencies: { string }?, init: ((self: any, kernel: any) -> ())?, start: ((self: any) -> ())?, stop: ((self: any) -> ())?, [any]: any,}| Field | Description |
|---|---|
Name |
Required, non-empty string, unique per kernel. Asserted at registration. |
Dependencies |
Names of services that must init and start before this one. Every listed name must be registered. |
init(self, kernel) |
Phase 1. Build own state. kernel is the booted kernel — on the server, the ServerKernel (session API included), not the bare core. |
start(self) |
Phase 2. Wire into other services; every dependency is fully initialized. |
stop(self) |
Reverse boot order on shutdown. |
| anything else | Yours. The definition table is the service instance — getService returns it directly. |
All three lifecycle functions are optional. A pure-data service with only a Name is valid.
API reference
Section titled “API reference”Game code normally uses the kernel wrappers (kernel:registerService, kernel:start(), kernel:getService); the manager is reachable at kernel.Services.
| Member | Description |
|---|---|
ServiceManager.new(kernel): ServiceManager |
Built by the kernel at boot; you never construct one. |
manager:register(definition: ServiceDefinition) |
Records a definition. Errors on missing Name, duplicates, or post-boot registration. |
manager:get(name: string): ServiceDefinition |
The service table. Errors unknown service "name" — no silent nil. |
manager:boot() |
Resolves order, runs all inits, then all starts. Called by kernel:start(). |
manager:stopAll() |
Stops in reverse boot order. Called by the kernel’s shutdown(). |
manager.BootOrder |
{ string } — the resolved order, available after boot. |
manager.Timings |
Per-service { Init: number, Start: number } seconds. |
manager.Booted |
true after boot(), false again after stopAll(). |
Error behavior
Section titled “Error behavior”Every failure mode has a distinct, loud error. Exact strings, from source:
| Situation | Result |
|---|---|
Definition without a Name |
service definition requires a Name (assert at registration). |
| Duplicate name | service "X" is already registered. |
| Registration after boot | cannot register service "X" after boot. |
| Dependency cycle | service dependency cycle: A -> B -> C -> A — the full chain, in path order. |
| Dependency on an unregistered name | service "A" depends on unregistered service "Ghost". |
init throws |
Boot aborts: service "X" failed during init: plus the traceback. Later services never init. |
start throws |
Boot aborts: service "X" failed during start: plus the traceback. Every init already ran. |
stop throws |
Warns (service "X" failed during stop:) and keeps stopping the rest — shutdown never gets stuck behind one broken service. |
get with an unknown name |
unknown service "name". |
Service timings in stats
Section titled “Service timings in stats”boot() clocks each service’s phases. manager.Timings[name] holds { Init, Start } in seconds, and the whole table surfaces as ServiceTimings in kernel:stats() — the first place to look when boot time grows:
for Name, Timing in Kernel:stats().ServiceTimings do print(string.format("%s: init %.2fms, start %.2fms", Name, Timing.Init * 1000, Timing.Start * 1000))endThe boot line ([ChloeKernel 0.5.0] Server booted N services in Xms) is the sum; ServiceTimings is the breakdown.