{"id":"synvasive/mirror","name":"mirror","scope":"synvasive","platform":"roblox","description":"Buffer-encoded table replication with per-player visibility, built on Blink","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"cb18000905a8a02ba41060c1620884a8311b67e9b1ce3062fde025f06b5c3265","likes":0,"downloads":0,"install":"forest install synvasive/mirror","url":"https://forest.dev/p/roblox/synvasive/mirror","files":"https://api.forest.dev/ai/package/roblox/synvasive/mirror/files","readme":"# Mirror\nLightweight table replication for Roblox. Mirror keeps a server table in sync with the clients that should see it, sending only what changed as compact buffers over [Blink](https://github.com/1Axen/Blink).\n\n## Features\n\n- **Delta replication**: Only changed paths are sent, batched once per frame\n- **Per-player visibility**: Replicate to everyone, or subscribe specific players\n- **Buffer encoding**: Numbers, strings, vectors, CFrames, colors and more are packed into a binary format\n- **String interning**: Repeated keys and short strings are sent once per player, then referenced by id\n- **Unreliable updates**: High-frequency values can skip the reliable queue\n- **Change coalescing**: Multiple writes to the same path in one frame send only the latest value\n- **Late joiners**: Players receive a full snapshot of everything visible to them when they load in\n- **Write functions**: Run the same mutation on the server and every client, sending only the function name and arguments\n- **Familiar API**: `Set`, `SetValues`, `TableInsert`, `TableRemove`, `Write`, `OnSet` and `FromId` work like Replica's, so most projects port with a few renames\n\n## Installation\n\n### Wally\nAdd Mirror to your `wally.toml`:\n```toml\n[dependencies]\nMirror = \"synvasive/mirror@1.0.0\"\n```\nThen run:\n```bash\nwally install\n```\n\n### Manual\nCopy the `src` folder into `ReplicatedStorage` and require `init.luau` as `Mirror`.\n\nMirror must be required on the client for anything to replicate. The client tells the server it's ready when the module loads.\n\n## Usage\n### Server\n\n```luau\nlocal Players = game:GetService(\"Players\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Mirror = require(ReplicatedStorage.Mirror)\n\nlocal Round = Mirror.new(\"Round\", {\n\tState = \"Intermission\",\n\tTime = 30,\n})\n\nRound:Replicate()\nRound:Set({ \"State\" }, \"Playing\")\nRound:SetUnreliable({ \"Time\" }, 29)\n\nPlayers.PlayerAdded:Connect(function(Player)\n\tlocal Inventory = Mirror.new(\"Inventory\", {\n\t\tOwner = Player.UserId,\n\t\tItems = {},\n\t})\n\n\tInventory:Subscribe(Player)\n\tInventory:Set({ \"Items\", 1 }, { Name = \"Iron Sword\", Damage = 10 })\nend)\n```\n\n### Client\n\n```luau\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal Mirror = require(ReplicatedStorage.Mirror)\n\nMirror.OnNew(\"Round\", function(Round)\n\tprint(Round.Data.State)\n\n\tRound:OnChange(function(Path, Value, Old)\n\t\tprint(table.concat(Path, \"/\"), Old, \"->\", Value)\n\tend)\nend)\n```\n\n## API\n\n### Server\n\n#### `Mirror.new(Token, Data?, WriteLib?)`\nCreates a reflection. `Token` is the name clients listen for with `Mirror.OnNew`. `Data` becomes the reflection's table. Read it through `Reflection.Data`, but write through the methods below so changes replicate. `WriteLib` is an optional ModuleScript of write functions, see [Write Functions](#write-functions).\n\n| Method | Description |\n|---|---|\n| `Reflection:Set(Path, Value)` | Sets a value at `Path` (an array of keys) and replicates it reliably |\n| `Reflection:SetUnreliable(Path, Value)` | Same as `Set`, but sent unreliably when possible. Use it for values where only the latest matters |\n| `Reflection:SetValues(Path, Values)` | Sets every key in `Values` inside the table at `Path` |\n| `Reflection:TableInsert(Path, Value, Index?)` | Inserts into the array at `Path` and returns the index it was inserted at |\n| `Reflection:TableRemove(Path, Index)` | Removes from the array at `Path` and returns the removed value |\n| `Reflection:Write(Name, ...)` | Runs a write function on the server and every client, and returns its results |\n| `Reflection:Replicate()` | Makes the reflection visible to every player |\n| `Reflection:DontReplicate()` | Undoes `Replicate`, so only subscribed players keep seeing the reflection |\n| `Reflection:Subscribe(Player)` | Makes the reflection visible to one player |\n| `Reflection:Unsubscribe(Player)` | Hides the reflection from a subscribed player |\n| `Reflection:Identify()` | Returns a readable `[Id:1;Token:Round]` string for debugging |\n| `Reflection:IsActive()` | Returns `false` once the reflection is destroyed |\n| `Reflection:Destroy()` | Removes the reflection from every client |\n\n#### `Mirror.FromId(Id)`\nReturns the reflection with that id, if it still exists.\n\n#### `Mirror.Flush()`\nSends all pending changes. Runs automatically every `Heartbeat`.\n\n### Client\n\n#### `Mirror.OnNew(Token, Callback)`\nCalls `Callback(Reflection)` for every existing and future reflection with that token. Returns a function that disconnects the listener.\n\n#### `Mirror.FromId(Id)`\nReturns the reflection with that id, if the client can see it.\n\n| Member | Description |\n|---|---|\n| `Reflection.Id` | Server-assigned id |\n| `Reflection.Token` | Token the reflection was created with |\n| `Reflection.Data` | The replicated table, kept up to date |\n| `Reflection:OnChange(Callback)` | Calls `Callback(Path, Value, Old)` after each change. Returns a disconnect function |\n| `Reflection:OnSet(Path, Callback)` | Calls `Callback(Value, Old)` when exactly `Path` changes. Returns a disconnect function |\n| `Reflection:OnWrite(Name, Callback)` | Calls `Callback(...)` with the arguments after a write function runs. Returns a disconnect function |\n| `Reflection:OnDestroy(Callback)` | Calls `Callback()` when the reflection is destroyed or hidden. Returns a disconnect function |\n| `Reflection:Identify()`, `Reflection:IsActive()` | Same as on the server |\n\nClient reflections also have `Set`, `SetValues`, `TableInsert`, `TableRemove` and `Write`. They only change the local copy and exist so write functions can run on the client.\n\n`OnChange` fires for every kind of change. For `TableInsert` and `TableRemove`, `Path` ends with the index, `Value` is the inserted value and `Old` is the removed value.\n\n## Write Functions\n\nA write function changes a reflection the same way on the server and on every client, so only its name and arguments are sent. Put them in a ModuleScript the client can see, such as one in `ReplicatedStorage`:\n\n```luau\nreturn {\n\tGive = function(Inventory, Item)\n\t\tInventory:TableInsert({ \"Items\" }, Item)\n\t\tInventory:Set({ \"Count\" }, #Inventory.Data.Items)\n\tend,\n}\n```\n\n```luau\nlocal Inventory = Mirror.new(\"Inventory\", { Items = {}, Count = 0 }, ReplicatedStorage.InventoryWrites)\nInventory:Write(\"Give\", \"Iron Sword\")\n```\n\nWrite functions must only depend on the reflection's data and their arguments, so every client reaches the same result as the server. Changes made inside them are not sent separately.\n\n## Supported Values\n\n`nil`, `boolean`, `number`, `string`, `table` (arrays and dictionaries, nested up to 64 levels), `Vector3`, `Vector2`, `CFrame`, `Color3`, `EnumItem`, `UDim`, `UDim2`, `BrickColor`, `NumberRange`, `Rect` and `buffer` are encoded into the buffer. Other values, such as Instances, are passed alongside it using Roblox's own serialization. Functions and threads are skipped.\n\n## Unreliable Updates\n\n`SetUnreliable` sends through an `UnreliableRemoteEvent`, so a change may be dropped or arrive out of order. Mirror falls back to the reliable channel when a change is larger than 900 bytes, uses a key the player hasn't received yet, or shares a frame with a `TableInsert`, `TableRemove` or `Write` on the same reflection. Avoid `SetUnreliable` on paths inside arrays you insert into or remove from, since a late unreliable change could land on a shifted index.\n\n## Networking\n\nThe networking layer in `src/Network` is generated by Blink from [`Mirror.blink`](Mirror.blink). To regenerate it:\n```bash\nblink Mirror.blink -y\n```\n\n## Examples\n\nWorking examples for a public round state and private per-player inventories can be found in [`src/Examples`](src/Examples).\n\n## License\n\nMIT, see [LICENSE](LICENSE)\n","readmeTruncated":false}