Skip to content

Milestones

Records once-per-session moments such as a loaded profile, so a subscriber that attaches late still receives every one that already happened.

Realm
Server
Require
ServerScriptService.ChloeKernelServer.Milestones
Since
v0.8.0
Source
Milestones.luau

A Bus topic is fire-once. A service that subscribes to Kernel.ProfileLoaded after a player’s profile loaded has missed that player for good, and whether it missed depends on boot order, join timing and load latency. That produced a whole class of intermittent cold-boot bugs: stations built at template size, boards never seeded, work never resumed. Milestones fixes it for moments that happen once per session. Milestones.publish records the moment on the session and then publishes the Bus topic. Milestones.on subscribes and then replays the recorded moment for every live session, so a late subscriber receives everything that already happened, once, and everything after. The framework publishes DataDriver profile loads, Passes lookups and Policy loads this way.

Milestones.publish(kernel, name, session, ...):

  1. When session is a table, stores table.pack(...) at session.Milestones[name], creating session.Milestones on first use. A later publish of the same name for the same session replaces the record.
  2. Publishes name on the Bus with (session, ...). Ordinary Bus subscribers receive it as usual, topic first.

Milestones.on(kernel, name, fn):

  1. Subscribes to name on the Bus first, so a publication that happens during the replay is not missed.
  2. Walks kernel.Sessions. For every session that has not ended and has a record for name, it calls fn(session, ...) with the recorded arguments on its own thread through task.spawn.
  3. Returns the Bus connection. Disconnecting it stops live deliveries.

The subscription remembers, per session, which record it last delivered (weak keys, so ended sessions are not kept). A live delivery of a record it already replayed is skipped, and so is a replay of a record it already received live. Each publication reaches each subscriber once, including one published from inside a replay handler while the walk is still running. A second publication for the same session is a new record and is delivered again.

fn receives (session, ...). Unlike a Bus handler, it gets no topic argument.

Topic Published by Arguments
Kernel.ProfileLoaded DataDriver, once the profile is set on session.Profile session, profile
Commerce.PassesResolved Passes, when every join lookup for the session finished session
Policy.Loaded Policy, when the player’s PolicyService rules loaded session, policy

kernel:registerService publishes Kernel.ServiceRegistered(name) after each successful registration (0.8.0). That is a plain Bus topic with no session to record it on, so Milestones.on does not replay it. Registration finishes before any service’s init runs, so code inside a service reads the registry directly: kernel.Services.Definitions lists every registered service. See Services.

Per-player setup that needs saved data:

src/Server/Game/Services/VisitService.luau
--!strict
-- VisitService: counts visits and stamps the last one on each profile.
local ServerScriptService = game:GetService("ServerScriptService")
local Milestones = require(ServerScriptService.ChloeKernelServer.Milestones)
local VisitService = {
Name = "VisitService",
Dependencies = { "DataService" },
}
function VisitService.init(self: any, kernel: any)
self.Kernel = kernel
end
function VisitService.start(self: any)
-- Also runs for every player whose profile loaded before this line
self.Connection = Milestones.on(self.Kernel, "Kernel.ProfileLoaded", function(session, profile)
profile.Data.Visits += 1
session:bind(function()
-- Runs before the DataDriver's final save, which is bound with bindLast
local Current = session.Profile
if Current then
Current.Data.LastVisit = os.time()
end
end)
end)
end
function VisitService.stop(self: any)
if self.Connection then
self.Connection:disconnect()
end
end
return VisitService

Visits and LastVisit must be in the profile’s Defaults. The leave-time write lands in the final save because DataDriver binds that save with session:bindLast, which runs after every ordinary binding.

A milestone of your own, for a moment that happens once per session:

local ServerScriptService = game:GetService("ServerScriptService")
local Milestones = require(ServerScriptService.ChloeKernelServer.Milestones)
-- PlotService, when it hands a player their plot
local function assignPlot(session: any, plot: Model)
session.Data.Plot = plot
Milestones.publish(kernel, "Plots.Assigned", session, plot)
end
-- Any other service, whenever it starts
Milestones.on(kernel, "Plots.Assigned", function(session, plot: Model)
local Sign = plot:FindFirstChild("OwnerSign", true)
if Sign and Sign:IsA("TextLabel") then
Sign.Text = session.Player.DisplayName
end
end)
Member Description
Milestones.publish(kernel, name: string, session, ...) Records ... on session.Milestones[name] when session is a table, then publishes name on the Bus with (session, ...).
Milestones.on(kernel, name: string, fn: (session, ...any) -> ()): Connection Subscribes, then replays the recorded moment for every live session. Delivers each publication once. Returns the Bus connection.
session.Milestones { [name]: packed arguments }, the moments recorded for this session.

Require it from ServerScriptService.ChloeKernelServer.Milestones. It is server only because sessions are.

  • A milestone needs a session. A server-wide moment (a round started, a map loaded) has nothing to be recorded on; keep that state in the owning service and let late readers ask it.
  • The record holds its arguments for the session’s life. For Kernel.ProfileLoaded that is the profile, which the session references anyway.
  • Replays skip sessions that have ended. A session that ends while its replay runs is the handler’s problem, as with any session callback: check session.Active after a yield.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.