{"id":"xopoiii/keepblox","name":"keepblox","scope":"xopoiii","platform":"roblox","description":"Player data for Roblox that loses nothing, fails loudly, and never stutters a frame","version":"0.5.0","latest":"0.5.0","versions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4.0","0.4.1","0.5.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"0b7706a4fd2a8f231768b2a884ebf1117e01ff1399abf5178c4c7f7ec08f1534","likes":0,"downloads":0,"install":"forest install xopoiii/keepblox","url":"https://forest.dev/p/roblox/xopoiii/keepblox","files":"https://api.forest.dev/ai/package/roblox/xopoiii/keepblox/files","readme":"# KeepBlox\n\nPlayer data for Roblox that loses nothing, fails loudly, and never stutters a frame.\n\nKeepBlox keeps session-locked player profiles on DataStore. It reads and writes the same records as\nProfileStore and speaks its lock protocol, so a game can switch by changing one `require`, run old and\nnew servers side by side during the rollout, and switch back.\n\n**Documentation: [xopoiii.github.io/KeepBlox](https://xopoiii.github.io/KeepBlox/)**\n\n> **Status: 0.5.0.** Everything below is proven in the simulator, against ProfileStore, five other\n> libraries and a game with no library. It is also checked on Roblox's real DataStore, MessagingService\n> and MemoryStore in a private test experience (`tests/live`), overload included. It has not yet run in a\n> live game with players: try it in a test place first.\n\n## Install\n\n- **Wally:** `KeepBlox = \"xopoiii/keepblox@0.5.0\"` under `[server-dependencies]`.\n- **pesde:** `pesde add xopoiii/keepblox -t roblox_server -a KeepBlox`.\n- **Studio:** `KeepBlox.rbxm` from the [latest release](https://github.com/XopoIII/KeepBlox/releases/latest).\n\nMore in [Installation](https://xopoiii.github.io/KeepBlox/getting-started/installation/).\n\n## Against the libraries games use today\n\nEvery library runs unmodified in the same simulator: the same fakes of DataStore, MemoryStore,\nMessagingService and the engine, the same players, faults and seeds (worst case over 20 seeds).\n\n| Library | Violations | A crash loses (steps of play) | Slowest open (s) | Failed opens | Requests per player-hour |\n|---|---|---|---|---|---|\n| **KeepBlox** | **0** | **4** | 9.4 | **0** | 13.1 + 13.1 |\n| ProfileStore | 0 | 287 | 46.6 | 20 | 13.6 + 13.6 |\n| ProfileService | 0 | 29 | 65.8 | 27 | 122 + 122 |\n| DocumentService | 0 | 0 (player locked out: 20 of 40 rejoins) | 15.9 | 389 | 25.9 + 25.9 |\n| Lapis | 0 | 0 (player locked out: 20 of 40 rejoins) | 11.9 | 374 | 14 + 14 |\n| DataStore2 | 61 | 287 | 8.8 | 0 | 0 + 1 |\n| Suphi's DataStore Module | 1 | 0 (player locked out: 20 of 40 rejoins) | 0.5 | 355 | 1 + 120 |\n| No library (GetAsync / SetAsync, as the Roblox guides teach) | 63 | 51 | 0.2 | 0 | 1 + 60.7 |\n\nRequests are data store reads + writes. KeepBlox also spends 45 MemoryStore units per player-hour on its\nserver heartbeat, which is how a dead server is known within seconds and how a crash loses only one\nbeat of play. The full tables, every scenario and the methodology are in\n[bench/Benchmarks.md](bench/Benchmarks.md); `luneblox run bench/Report 20` makes them.\n\n## What it promises\n\nEach promise is a spec, and the simulation checks the data invariants after every write, across many\nvirtual servers and random fault schedules:\n\n- An acknowledged save is never lost, and only one server can write a profile at a time.\n- A hand-over request never ends the wrong session, a load that gives up never leaves its server holding\n  the key, and a live server whose MemoryStore hangs is never taken for dead.\n- A crash loses about one 4 s heartbeat of play: each beat carries a snapshot the next server takes.\n- A failed load never writes; data that is not a profile is quarantined, never overwritten.\n- A server that loses the session freezes its copy at once, so trades and purchases stop on stale data.\n- One bad value (NaN, bad UTF-8, a cycle) never costs the play around it: it is repaired in what is\n  stored and reported with its path. What cannot be repaired (over 4 MB, a mixed table, an array with\n  holes, number keys: shapes Roblox would silently cut or rename) is refused, and the last good snapshot\n  is stored instead.\n- Offline messages are consumed exactly once; purchases are granted once and never lost.\n- Shutdown releases every profile within the deadline; no call retries forever, and a load answers within\n  `loadTimeout` in all.\n- Under overload the budget goes to saves first: a load waits for the server's budget instead of polling\n  it, so the saves that hand players over are not throttled.\n- Saving is spread across frames and respects the DataStore request budget: a 1 MB profile's save costs\n  at most about 4.1 ms in any one frame.\n- A release can be rolled back without locking out a player who played on it (`writeVersion`).\n\nThe checker is checked too: 31 small slips in the code that keeps these promises each make the suite\nfail (`tests/Mutate.luau`), and the save check is fuzzed against the store's encoding.\n\n## What it gives you\n\n- Offline and admin edits that never kick a player (`store:edit`), and offline messages (`store:message`).\n- Trades between two profiles that land in both or in neither (`store:trade`).\n- Shared documents for guilds and clans, updated atomically from any server (`KeepBlox.shared`).\n- Leaderboards mirrored to ordered data stores (`leaderboards`).\n- A schema with numbered, testable migrations; deep defaults for template stores.\n- Releases you can roll back: migrations with a way back (`{ up, down }`) and `writeVersion`, so the\n  release before reads everything, renames included. Lapis and DocumentService's `backwardsCompatible`\n  covers only changes old code can read as they are.\n- Purchases through `KeepBlox.processReceipt`, and version history with a safe restore that names the\n  purchases it takes back. Optional compression for large profiles, and Studio modes that never touch\n  live data.\n- Types for Luau (`--!strict` throughout) and roblox-ts (`types/index.d.ts`).\n\n## Moving to KeepBlox\n\nFrom ProfileStore or ProfileService, change one line; the keys and their format stay as they are:\n\n```lua\nlocal ProfileStore = require(path.to.KeepBlox.Compat.ProfileStore)\n```\n\nFrom DocumentService, Lapis, DataKeep, DataStore2 or Suphi's DataStore Module, point a store at the old\ndata: each key moves on its first load, and the old data is only read.\n\n```lua\nlocal store = KeepBlox.store(\"Profiles\", {\n\ttemplate = { coins = 0 },\n\timport = KeepBlox.importers.DocumentService({ store = \"PlayerData\" }),\n})\n```\n\n## Development\n\n```sh\nrokit install          # the pinned toolchain\nlefthook install       # the gates, before every commit\nsh scripts/run-tests.sh\nluneblox run tests/Mutate --yes   # mutation adequacy: every slip must fail the suite (about 16 min)\nluneblox run bench/download && luneblox run bench/Report 20   # the benchmarks\nluneblox run tests/live/Serve    # the live check, only in the test experience (tests/live/README.md)\nnpm ci --prefix docs && npm run build --prefix docs          # the documentation site\n```\n\nTests run on [LuneBlox](https://github.com/XopoIII/LuneBlox), which runs the Luau version and fast flags\nRoblox runs.\n\n## License\n\nMIT. See [LICENSE](LICENSE). Vendored reference code in `tests/reference/` keeps its own license.\n","readmeTruncated":false}