Profile snapshots
Keeps in-memory deep copies of one DataDriver profile's data as undo points, restores them in place, and never writes them to storage.
- Realm
- Server
- Require
ServerScriptService.ChloeKernelServer .Data - Since
- v0.7.0
- Source
- Data/init.luau
Profile snapshots are in-memory undo points for one DataDriver profile. profile:snapshot(label?) deep-copies profile.Data into a ring that keeps the newest 8, and profile:rollback(id) copies it back. The defining decision is that snapshots never touch storage: they exist for admin tooling and for guarding one risky mutation on one profile, and a rollback persists only when you save. Changes that must move two profiles together belong in Transactions, which records one durable decision for both. Corruption across sessions is what version history and backups are for.
Mental model
Section titled “Mental model”A snapshot is a deep copy. Every nested table in profile.Data is rebuilt, so later writes to Data never reach the stored copy. The ring keeps the newest 8: a ninth snapshot evicts the oldest without a warning, and rolling back to an evicted id returns false, "no snapshot <id>".
Rollback restores in place. Keys the snapshot lacks are removed, keys it has are written back, and nested tables are restored recursively. profile.Data and its subtables stay the same table objects, so references that services already hold stay live. Before 0.8.0 rollback replaced profile.Data with a new table and left those references pointing at stale data.
A rollback reads the snapshot and copies it, so the stored entry is never consumed. Rolling back to the same id twice gives the same state both times.
Snapshots live in server memory on the Profile object. They are never written to the backend, and they are gone once the profile is released or the server closes.
Take a snapshot immediately before a risky mutation, change Data directly, and roll back if the result is wrong:
-- Admin grant: undo the whole change when it would leave the player in debtlocal function grantWithGuard(profile: any, itemId: string, count: number, itemCost: number): (boolean, string?) local Id = profile:snapshot("before admin grant")
profile.Data.Inventory[itemId] = (profile.Data.Inventory[itemId] or 0) + count profile.Data.Coins -= count * itemCost
if profile.Data.Coins < 0 then profile:rollback(Id) return false, "insufficient coins" end
-- Rollback alone never touches the backend; the grant persists with this save return profile:save()endAn admin panel can list the ring with profile:snapshots(), which returns { Id, Label, TakenAt, Data } entries, oldest first. TakenAt is os.clock() on this server.
API reference
Section titled “API reference”| Member | Description |
|---|---|
profile:snapshot(label?) -> number |
Deep-copies profile.Data into the ring and returns its id. Ids increase per profile. label is free text for your own bookkeeping. |
profile:snapshots() -> { { Id, Label?, TakenAt, Data } } |
The ring itself, oldest first. Treat it as read-only: changing an entry’s Data changes what a rollback restores. |
profile:rollback(id) -> (ok, err?) |
Restores that snapshot’s data into profile.Data in place. false, "no snapshot <id>" when the id was never taken or has been evicted. |
Gotchas
Section titled “Gotchas”- A rollback changes memory only. Call
profile:save()when the rolled-back state must survive, or let the next autosave or the leave save write it. - Autosaves keep running between a snapshot and its rollback. If the guarded code yields, a save in that window persists the mutated state, and the rollback only fixes memory until the next save. Keep snapshot, mutation and rollback in one synchronous block.
- The ring evicts past 8 entries. Do not rely on an id staying valid while other code keeps taking snapshots of the same profile.
- A snapshot holds a full copy of the data. A large profile times 8 entries is real memory, so take snapshots around specific operations only.