{"id":"synvasive/cabinetservice","name":"cabinetservice","scope":"synvasive","platform":"roblox","description":"Session-locked DataStore solution with automatic client replication","version":"1.1.5","latest":"1.1.5","versions":["1.0.0","1.0.1","1.1.0","1.1.1","1.1.2","1.1.3","1.1.4","1.1.5"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"32a9cabb58606f46f99273cc81a66dc1e512e7113206f7200282892633716939","likes":0,"downloads":0,"install":"forest install synvasive/cabinetservice","url":"https://forest.dev/p/roblox/synvasive/cabinetservice","files":"https://api.forest.dev/ai/package/roblox/synvasive/cabinetservice/files","readme":"# CabinetService\r\nAn intuitive wrapper for DocumentService with automatic client replication based on DataService by leif.\r\n\r\n## Features\r\n\r\n- **Session locking** — prevents data corruption across multiple servers\r\n- **Automatic replication** — server changes propagate to the client automatically\r\n- **Player & non-player stores** — works for both player data and shared documents like guilds or worlds\r\n- **Data object API** — operate directly on a typed data object rather than through a global service\r\n- **Reactive listeners** — `OnChanged`, `OnArrayInserted`, `OnArrayRemoved` on both server and client\r\n- **No interface required** — CabinetService generates a type validator from your template automatically\r\n\r\n## Installation\r\n\r\n### Wally\r\nAdd CabinetService to your `wally.toml`:\r\n```toml\r\n[dependencies]\r\nCabinetService = \"synvasive/cabinetservice@1.0.0\"\r\n```\r\nThen run:\r\n```bash\r\nwally install\r\n```\r\n\r\n### Roblox\r\nGo to https://create.roblox.com/store/asset/102495465895441/CabinetService and download the model\r\n\r\n### Manual\r\nCopy the `src` folder into your project and require `init.luau` as `CabinetService`.\r\n\r\n## Usage\r\n### Server — Player Store\r\n\r\n```luau\r\nlocal Players = game:GetService(\"Players\")\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\n\r\nlocal CabinetService = require(ReplicatedStorage.CabinetService).Server\r\nlocal CabinetName = \"Data\"\r\n\r\nlocal Template = {\r\n\tLevel = 1,\r\n\tGold = 0,\r\n\tExp = 0,\r\n\r\n\tInventory = {},\r\n\tSettings = {\r\n\t\tVolume = 0.5,\r\n\t},\r\n}\r\n\r\nlocal PlayerStore = CabinetService.Init {\r\n\tName = CabinetName,\r\n\tTemplate = Template,\r\n}\r\n\r\nPlayers.PlayerAdded:Connect(function(Player)\r\n\tlocal Data = PlayerStore.WaitForData(Player.UserId)\r\n\r\n\tData:Increment(\"Exp\", 50)\r\n    Data:Increment(\"Coins\", 100)\r\n\r\n\tData:Set(\"Settings/Volume\", 0)\r\n    Data:ArrayInsert(\"Inventory\", {\r\n        Name = \"Iron Sword\",\r\n        Damage = 10,\r\n    })\r\n\r\n\tData:OnChanged(\"Level\", function(Current, Previous)\r\n\t\tprint(Player.Name .. \" leveled up! \" .. Previous .. \" → \" .. Current)\r\n\tend)\r\nend)\r\n\r\n```\r\n\r\n### Client\r\n\r\n```luau\r\nlocal Players = game:GetService(\"Players\")\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\n\r\nlocal CabinetService = require(ReplicatedStorage.CabinetService).Client\r\nCabinetService.Init()\r\n\r\nlocal Player = Players.LocalPlayer\r\nlocal Entry = CabinetService.WaitForEntry(\"Data\", Player.UserId)\r\n\r\nprint(\"Coins:\", Entry:Get(\"Coins\"))\r\nprint(\"Volume:\", Entry:Get(\"Settings/Volume\"))\r\n\r\nEntry:OnChanged(\"Coins\", function(Current, Previous)\r\n\tprint(\"Coins changed from\", Previous, \"to\", Current)\r\nend)\r\n\r\nEntry:OnArrayInserted(\"Inventory\", function(Index, Item)\r\n\tprint(\"Item inserted at index\", Index, \":\", Item)\r\nend)\r\n```\r\n\r\n### Server — Non-Player Store\r\n\r\nFor shared documents like guilds, worlds, or any data not tied to a specific player:\r\n\r\n```luau\r\nlocal CabinetService = require(ReplicatedStorage.CabinetService)\r\nlocal Server = CabinetService.Server\r\n\r\nlocal GUILD_TEMPLATE = {\r\n    Name = \"Unnamed Guild\",\r\n    Bank = 0,\r\n    Members = {},\r\n}\r\n\r\n-- PlayerStore = false — you control when documents are opened\r\nlocal GuildStore = Server.Init({\r\n    Template = GUILD_TEMPLATE,\r\n    DocumentName = \"GuildData\",\r\n    PlayerStore = false,\r\n})\r\n\r\nlocal function GetGuildData(GuildId: string)\r\n    -- Opens the document if not already open, then yields until ready\r\n    return GuildStore.WaitForData(GuildId)\r\nend\r\n\r\nlocal function Deposit(GuildId: string, Amount: number)\r\n    local Data = GetGuildData(GuildId)\r\n    Data:Increment(\"Bank\", Amount)\r\nend\r\n```\r\n\r\n## API\r\n\r\n### `Server.Init(Options)`\r\n\r\nInitialises a store and returns a handle. Must be called on the server.\r\n\r\n| Option | Type | Default | Description |\r\n|---|---|---|---|\r\n| `Name` | `string` | - | DataStore key name |\r\n| `Template` | `T` | - | Default data shape, also used to generate the type validator |\r\n| `PlayerStore` | `boolean` | `true` | If true, manages PlayerAdded/PlayerRemoving automatically |\r\n| `Interface` | `((T) -> boolean)?` | auto | Custom type validator — generated from template if omitted |\r\n| `LockSessions` | `boolean` | `true` | Enables session locking |\r\n| `UseMock` | `boolean` | `false` | Uses a mock DataStore, data is erased on close |\r\n| `DontSave` | `boolean` | `false` | Data is erased on server close |\r\n| `ResetData` | `boolean` | `false` | Erases data on load |\r\n| `ViewedUserId` | `number?` | - | Load a different user's data |\r\n| `OverridenUserId` | `number?` | - | Override the userId used for the document key |\r\n\r\n### `Store.WaitForData(Id)`\r\nYields until the document for `Id` is open and returns the `Data` object. Safe to call multiple times — returns immediately if already loaded.\r\n\r\n### Data Object Methods\r\n\r\n| Method | Description |\r\n|---|---|\r\n| `Data:Get(Path?)` | Get a value by `/`-separated path, or the full table if no path |\r\n| `Data:Set(Path, Value)` | Set a value at path |\r\n| `Data:Increment(Path, Amount)` | Increment a number at path |\r\n| `Data:Update(Path, Callback)` | Update a value with a transform function |\r\n| `Data:ArrayInsert(Path, Value, Index?)` | Insert into an array at path |\r\n| `Data:ArrayRemove(Path, Index)` | Remove from an array at path |\r\n| `Data:OnChanged(Path, Callback)` | Fire callback when a value changes |\r\n| `Data:OnArrayInserted(Path, Callback)` | Fire callback when an item is inserted |\r\n| `Data:OnArrayRemoved(Path, Callback)` | Fire callback when an item is removed |\r\n\r\n### `Server.Mutate(Name, Id, Mutator)`\r\nPerforms an atomic multi-field write directly through the DataStore. Use this when multiple fields must be saved together as a single write — for everything else, use the Data object methods.\r\n\r\n### `Client.Init()`\r\nMust be called once on the client before any other Client method.\r\n\r\n### `Client.WaitForEntry(Name, Id)`\r\nYields until the server has replicated the document for `Id` and returns the local `Data` object.\r\n\r\n### `Client.Fetch(Name, Id, Path?)`\r\nPerforms a one-off remote read from the server. Useful before replication has arrived.\r\n\r\n## Examples\r\n\r\nFull working examples for player data and guild data can be found in [`src/Examples`](src/Examples).\r\n\r\n## License\r\n\r\nMIT — see [LICENSE](LICENSE)","readmeTruncated":false}