{"id":"breezy1214/ezprofilestore","name":"ezprofilestore","scope":"breezy1214","platform":"roblox","description":"A typed ProfileStore wrapper with draft-based updates and built-in client replication.","version":"0.1.5","latest":"0.1.5","versions":["0.1.5"],"license":"MIT","licenseRating":"safe","licenseCaveats":["The package archive does not include its license text; the license is declared in its manifest metadata."],"licenseVerified":false,"dependencies":{"elttob/fusion":{"version":"^0.3.0","alias":"Fusion"},"evaera/promise":{"version":"^4.0.0","alias":"Promise"},"sleitnick/signal":{"version":"^2.0.3","alias":"Signal"},"lm-loleris/profilestore":{"version":"^1.0.3","alias":"ProfileStore"}},"integrity":"95ce05ee96b7bd2027c067327e6bf6ad6ea38191655010611d557d7ed6cf2868","likes":0,"downloads":0,"install":"forest install breezy1214/ezprofilestore","url":"https://forest.dev/p/roblox/breezy1214/ezprofilestore","files":"https://api.forest.dev/ai/package/roblox/breezy1214/ezprofilestore/files","readme":"# EZProfileStore\n\nA typed [ProfileStore](https://github.com/MadStudioRoblox/ProfileStore) wrapper. The server edits player data as plain Luau; only the changes replicate to the client.\n\n## Installation\n\n```toml\n[dependencies]\nEZProfileStore = \"breezy1214/ezprofilestore@0.1.5\"\n```\n\nRequires Luau's new type solver (luau-lsp: `luau-lsp.fflags.enableNewSolver`).\n\n## Data\n\nShare one template between server and client:\n\n```lua\n-- ReplicatedStorage/PlayerData.luau\nexport type Data = {\n\tCoins: number,\n\tInventory: { string },\n\tStats: { Wins: number, Losses: number },\n}\n\nlocal template: Data = {\n\tCoins = 0,\n\tInventory = {},\n\tStats = { Wins = 0, Losses = 0 },\n}\n\nreturn { template = template }\n```\n\n## Server\n\n```lua\nlocal store = EZProfileStore.Server.new({\n\tstoreName = \"PlayerData_v1\",\n\ttemplate = PlayerData.template,\n\tmode = \"studio\", -- mock data in Studio, live data elsewhere\n})\n\nstore:Update(player, function(data)\n\tif data.Coins < 25 then\n\t\treturn false -- abort, nothing changes\n\tend\n\tdata.Coins -= 25\n\ttable.insert(data.Inventory, \"Potion\")\nend)\n\nlocal data = store:Get(player) -- read-only; change data with Update\n```\n\n`Update` commits all-or-nothing. It returns `false` if the function errors, yields, returns `false`, or writes a value DataStores can't save. Use `for k, v in data` (not `pairs`) inside it, and don't keep `data` after it returns.\n\nTrades across players:\n\n```lua\nstore:UpdateMany({ buyer, seller }, function(drafts)\n\tif drafts[buyer].Coins < price then\n\t\treturn false\n\tend\n\tdrafts[buyer].Coins -= price\n\tdrafts[seller].Coins += price\nend)\n```\n\n## Client\n\n```lua\nlocal client = EZProfileStore.Client.new({ template = PlayerData.template })\n\nlocal wins = client:Select(function(data)\n\treturn data.Stats.Wins\nend)\n\nwins:Get() -- number?\nwins:Observe(function(new, old) end) -- returns a disconnect function\nwins.State -- Fusion state, e.g. use(wins.State)\nwins:Destroy()\n```\n\n## Config\n\n```lua\nlocal config: EZProfileStore.ServerConfig<PlayerData.Data> = {\n\tstoreName = \"PlayerData_v1\",\n\ttemplate = PlayerData.template,\n\n\tleaderstats = {\n\t\t{ name = \"Coins\", type = \"IntValue\", value = function(data) return data.Coins end },\n\t\t{ name = \"Games\", type = \"IntValue\", value = function(data) return data.Stats.Wins + data.Stats.Losses end },\n\t},\n\n\tprivatePaths = { -- never sent to the client\n\t\tfunction(data) return data.Inventory end,\n\t},\n\n\tvalidate = function(data) -- checked on load and before every Update commits\n\t\treturn data.Coins >= 0, \"Coins must not be negative\"\n\tend,\n}\n\nlocal store = EZProfileStore.Server.new(config)\n```\n\nAnnotating the config as `ServerConfig<Data>` gives the functions inside it full type checking.\n\nOther options: `keyPrefix`, `mode` (`\"live\"`, `\"studio\"`, `\"mock\"`), `migrations`, `migrateNewProfiles`, `remoteParent`, `autoLoadPlayers`, `store`, `profileStore`.\n\n## Migrations\n\n```lua\nmigrations = EZProfileStore.Migrations.new({\n\t{\n\t\tname = \"move-coins\",\n\t\tup = function(data)\n\t\t\tdata.Currency = { Coins = data.Coins or 0 }\n\t\t\tdata.Coins = nil\n\t\tend,\n\t},\n}),\n```\n\nAppend a migration whenever the stored shape changes. Only versions newer than the profile's run, before `Reconcile`. New profiles skip them unless `migrateNewProfiles = true`. A failing migration restores the data and fires `ProfileLoadFailed`.\n\n## Testing\n\n```lua\nlocal store = EZProfileStore.Server.mock({\n\tstoreName = \"Test\",\n\ttemplate = template,\n\tautoLoadPlayers = false,\n})\n```\n\nTo fake the backend, pass `store = fakeStore` or `profileStore = fakeModule`.\n\n## API\n\n**Server**\n\n| Member | Description |\n| --- | --- |\n| `Server.new(config)` / `Server.mock(config)` | Create a store. |\n| `Get(player)` / `GetAsync(player, timeout?)` | Read-only data. |\n| `Update(player, fn)` / `UpdateMany(players, fn)` | Edit data, all-or-nothing. |\n| `Changed`, `FirstJoin`, `ProfileLoaded`, `ProfileReleased`, `ProfileLoadFailed` | Signals. |\n| `PreSave`, `PostSave`, `LastSave`, `Migrated`, `RolledBack` | Signals. |\n| `GetProfile(player)` / `GetProfileAsync(player, timeout?)` | The ProfileStore profile. |\n| `IsReady(player)`, `SaveAsync(player)`, `GetKey(target)`, `GetStore()` | Utilities. |\n| `SendMessageAsync(target, message)` / `OnMessage(handler)` | Offline messages. |\n| `PreviewRollbackAsync(target, selector?)` / `ApplyRollbackAsync(preview)` / `RollbackAsync(target, selector?)` | Restore an older save. The selector is `{ stepsBack }`, `{ before }`, or `{ version }`. |\n| `WipeAsync(target)` | Delete a profile. |\n| `Destroy()` | End sessions and disconnect. |\n\n**Client**\n\n| Member | Description |\n| --- | --- |\n| `Client.new(config?)` | Connect and request the snapshot. |\n| `Ready`, `IsReady()`, `OnReady(fn)` | Load state. |\n| `Get()` / `GetAsync(timeout?)` | Read-only data. |\n| `Select(selector)` | A `Selection` with `Get`, `Observe`, `State`, and `Destroy`. |\n| `Destroy()` | Disconnect. |\n\nExported types: `Server<Data>`, `Client<Data>`, `Selection<V>`, `ReadOnly<T>`, `ServerConfig<Data>`, `ClientConfig<Data>`, `Profile<Data>`, `LeaderstatEntry<Data>`.\n\n## License\n\nMIT\n","readmeTruncated":false}