{"id":"stratiz/DataStream","name":"DataStream","scope":"stratiz","platform":"roblox","description":"A real-time data replication solution","version":"2.1.0","latest":"2.1.0","versions":["1.0.0","1.0.1","2.0.0","2.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"stratiz/Cleaner":{"version":"^1.0.0"},"stratiz/Signal":{"version":"^1.0.0"}},"integrity":"0c358a1a10eac21ed53664ee68443c13665b694f9e66917f054e39fcde35548e","likes":2,"downloads":4,"install":"forest install stratiz/DataStream","url":"https://forest.dev/p/roblox/stratiz/DataStream","files":"https://api.forest.dev/ai/package/roblox/stratiz/DataStream/files","readme":"# DataStream\r\n\r\nDataStream is a intuitive ReplicaService alternative. All schemas are replicated in real time (no loops!) between the client and server with no need to call obnoxious methods.\r\n\r\nDataStreams can be used for anything from PlayerData to NPC data replication. You can even put Instances in your data as values or even as table keys! If an instance hasn't replicated to a client yet (StreamingEnabled), DataStream tracks it and links it up automatically once it arrives, even if it streams out and back in.\r\n\r\nRecommended for use with projects that use external editors such as VSCode\r\n\r\n## Immutability\r\n\r\nNo more deep copies! Everything you get out of a stream is frozen (`table.freeze`), so reading data is basically free. If you try to modify a table you read, it'll error instead of silently desyncing your data from the server. If you want to modify it, just clone it:\r\n\r\n```lua\r\nlocal stats = DataStream.GameData.Stats:Read()\r\nstats.TotalDeaths += 1 -- ERROR: attempt to modify a readonly table\r\n\r\nlocal mutable = table.clone(stats) -- clone it if you need your own copy\r\n```\r\n\r\nAs a bonus, since writes swap tables out instead of modifying them, anything you `:Read()` is a snapshot that will never change out from under you, even after later writes to the same path.\r\n\r\n## Table of Contents\r\n- [DataStream](#datastream)\r\n  - [Immutability](#immutability)\r\n  - [Table of Contents](#table-of-contents)\r\n  - [Schemas](#schemas)\r\n    - [Global:](#global)\r\n    - [Player:](#player)\r\n    - [Registering schemas](#registering-schemas)\r\n  - [Methods `DataStreamObject`](#methods-datastreamobject)\r\n    - [**:Read()**](#read)\r\n    - [**:Write()**](#write)\r\n    - [**:Changed((newValue : any, oldValue : any) -\\> ())**](#changednewvalue--any-oldvalue--any---)\r\n    - [**:ChildAdded((indexOfChild : any) -\\> ())**](#childaddedindexofchild--any---)\r\n    - [**:ChildRemoved((indexOfChild : any) -\\> ())**](#childremovedindexofchild--any---)\r\n    - [**:Insert(value : any)**](#insertvalue--any)\r\n    - [**:Remove(value : any)**](#removevalue--any)\r\n  - [Examples](#examples)\r\n    - [1. Increase playtime each second for a player:](#1-increase-playtime-each-second-for-a-player)\r\n    - [2. Adding and removing players to an array](#2-adding-and-removing-players-to-an-array)\r\n  - [Installation](#installation)\r\n\r\n\r\n## Schemas\r\n\r\nA schema is a template data set DataStream starts with. In DataStream, there are two types of schemas:\r\n\r\n1. Global Schemas\r\n   \r\n   Global schemas are a single data set that is initialized immediately that is shared in real-time between all players and the server.\r\n   \r\n2. Player Schemas \r\n   \r\n    Player schemas are a data set that is unique to each individual player, and are initialized as each player joins.\r\n\r\nFor our examples, we will be using the following schemas\r\n\r\n### Global:\r\n```lua\r\nreturn { --Schemas/Global/GameData.lua\r\n    CurrentGameTime = 0,\r\n    GlobalPlaytime = 0,\r\n    PlayerInGame = {},\r\n    CurrentGameMessage = \"Intermission\",\r\n    Stats = {\r\n        TotalDeaths = 0,\r\n        CoinsCollected = 0,\r\n        ObjectsCollected = {}\r\n    }\r\n}\r\n```\r\n\r\n### Player:\r\n```lua\r\nreturn { --Schemas/Player/Stored.lua\r\n    Currency = {\r\n        Coins = 0,\r\n        Gems = 0\r\n    },\r\n    PlaytimeSeconds = 0\r\n}\r\n```\r\n\r\n### Registering schemas\r\n\r\nNo special folders needed — just require DataStream from a server script and add the stream itself via the methods:\r\n\r\n```lua\r\n-- Server\r\nlocal DataStream = require(ReplicatedStorage.DataStream).Server\r\n\r\nDataStream:MakeGlobalStream(\"GameData\", require(script.Schemas.GameData))\r\nDataStream:AddPlayerStreamTemplate(\"Stored\", require(script.Schemas.Stored))\r\n```\r\n\r\nThe only rule: register your streams as soon as your script runs, with no yields before the registration calls (no `task.wait`, no `WaitForChild`, etc).\r\n\r\nIf another script might index a stream before the registering script has run, use `WaitForSchema` — it yields until the stream exists, just like `WaitForChild` (including the warning if it's taking suspiciously long):\r\n\r\n```lua\r\nlocal gameData = DataStream:WaitForSchema(\"GameData\")\r\ngameData.CurrentGameMessage = \"Hello!\"\r\n```\r\n\r\nPlain indexing (`DataStream.GameData`) doesn't wait: on the server it errors if the schema isn't registered, and on the client it returns `nil` until the schema's data has arrived. This is a Luau limitation — index operations run inside metamethods, which aren't allowed to yield.\r\n\r\nRegistering a stream late (after players are already in game) is also fine: DataStream will push the stream's data to connected clients when it's registered, and any `WaitForSchema` calls waiting on it will resolve.\r\n\r\n\r\n## Methods `DataStreamObject`\r\n**All methods are the same on the server and client.**\r\n\r\n### **:Read()**\r\nReads the current value that the StreamObject references.\r\n\r\n```lua\r\nlocal value = DataStream.SchemaName.ValueName:Read()\r\n\r\nprint(\"The current value of ValueName is\", value)\r\n```\r\n\r\n### **:Write()**\r\n**SERVER ONLY** Writes the current value that the StreamObject references.\r\n\r\n```lua\r\n-- There are many ways to perform a write operation:\r\nDataStream.SchemaName.ValueName:Write(10)\r\nDataStream.SchemaName.ValueName = 10\r\n\r\n-- Math operators\r\nDataStream.SchemaName.ValueName *= 10\r\nDataStream.SchemaName.ValueName /= 10\r\nDataStream.SchemaName.ValueName += 10\r\nDataStream.SchemaName.ValueName -= 10\r\n```\r\n\r\n### **:Changed((newValue : any, oldValue : any) -> ())**\r\nFires a callback function when the referenced value is changed. The callback also receives the value from before the change — both are frozen snapshots, so they stay stable even as more writes happen.\r\n\r\n```lua\r\nDataStream.SchemaName.ValueName:Changed(function(newValue, oldValue)\r\n    print(\"Value changed from\", oldValue, \"to\", newValue)\r\nend)\r\n\r\nDataStream.SchemaName.ValueName = 10\r\n```\r\n\r\n### **:ChildAdded((indexOfChild : any) -> ())**\r\nFires a callback function when the referenced dictionary has a new member.\r\n\r\n```lua\r\nDataStream.SchemaName.ValueName = {}\r\nDataStream.SchemaName.ValueName:ChildAdded(function(newIndex)\r\n    print(\"New value is equal to\", DataStream.SchemaName.ValueName[newIndex]:Read())\r\nend)\r\n\r\nDataStream.SchemaName.ValueName.NewValue = \"Hello world!\"\r\n```\r\n\r\n### **:ChildRemoved((indexOfChild : any) -> ())**\r\nFires a callback function when the referenced dictionary loses a member.\r\n\r\n```lua\r\nDataStream.SchemaName.ValueName = {\r\n    NewValue = \"Hello World!\"\r\n}\r\nDataStream.SchemaName.ValueName:ChildRemoved(function(newIndex)\r\n    print(\"New value is equal to\", DataStream.SchemaName.ValueName[newIndex]:Read())\r\nend)\r\n\r\nDataStream.SchemaName.ValueName.NewValue = nil\r\n```\r\n\r\n### **:Insert(value : any)**\r\n**:Insert(position : number, value : any)**\r\n\r\nInserts the provided value to the target position of the array. If target position is not provided, it will append at the end of the array.\r\n\r\n```lua\r\nDataStream.SchemaName.NewArray = {}\r\n\r\nDataStream.SchemaName.NewArray:Insert(\"Hello,\")\r\nDataStream.SchemaName.NewArray:Insert(\"world!\")\r\n\r\nprint(table.concat(DataStream.SchemaName.NewArray:Read(), \" \")) --> \"Hello, world!\"\r\n```\r\n\r\n### **:Remove(value : any)**\r\n\r\nRemoves the specified element from the array, shifting later elements down to fill in the empty space if possible.\r\n\r\n```lua\r\nDataStream.SchemaName.NewArray = {\"a\", \"b\", \"c\"}\r\n\r\nDataStream.SchemaName.NewArray:Remove(2)\r\nDataStream.SchemaName.NewArray:Remove(2)\r\n\r\nprint(DataStream.SchemaName.NewArray:Read()) --> { \"a\" }\r\n```\r\n\r\n\r\n\r\n## Examples\r\n\r\n*Note: These are all for example sake, some of these methods may not be the most efficient solutions depending on your use-case.*\r\n\r\n### 1. Increase playtime each second for a player:\r\n\r\n```lua\r\n-- Server\r\nlocal Players = game:GetService(\"Players\")\r\nlocal DataStream = require(ReplicatedStorage.DataStream).Server\r\n\r\nlocal globalGameDataStream = DataStream.GameData\r\n\r\nlocal function SetupPlayer(player : Player)\r\n    local playerStoredStream = DataStream.Stored[player]\r\n\r\n    task.spawn(function()\r\n        while player.Parent and task.wait(1) do\r\n            playerStoredStream.PlaytimeSeconds += 1\r\n            globalGameDataStream.GlobalPlaytime += 1\r\n        end\r\n    end)\r\nend\r\n\r\n\r\n-- Client\r\n\r\nlocal DataStreamClient = require(ReplicatedStorage.DataStream).Client\r\n\r\nDataStreamClient.Stored.PlaytimeSeconds:Changed(function(seconds : number)\r\n    print(\"Current player seconds:\", seconds)\r\nend)\r\n\r\n```\r\n\r\n### 2. Adding and removing players to an array\r\n\r\n```lua\r\n-- Server\r\nlocal Players = game:GetService(\"Players\")\r\nlocal DataStream = require(ReplicatedStorage.DataStream).Server\r\n\r\nlocal globalGameDataStream = DataStream.GameData\r\n\r\nfunction AddPlayerToGame(player)\r\n    globalGameDataStream.PlayersInGame:Insert(player)\r\nend\r\n\r\nfunction RemovePlayerFromGame(player)\r\n    local index = table.find(globalGameDataStream.PlayersInGame:Read(), player)\r\n    if index then\r\n        globalGameDataStream.PlayersInGame:Remove(index)\r\n    end\r\nend\r\n\r\n\r\n-- Client\r\n\r\nlocal DataStreamClient = require(ReplicatedStorage.DataStream).Client\r\n\r\nlocal LocalPlayer = game.Players.LocalPlayer\r\nlocal PlayerInGameStream = DataStreamClient.GameData.PlayersInGame\r\n\r\nfunction isLocalPlayerInGame() : boolean\r\n    return table.find(PlayerInGameStream:Read(), LocalPlayer) ~= nil\r\nend\r\n```\r\n\r\n## Installation\r\n\r\nDataStream is available on the Forest Package Manager:\r\n**https://forest.dev/p/roblox/stratiz/datastream**","readmeTruncated":false}