{"id":"coffilhg/coffeeobjects","name":"coffeeobjects","scope":"coffilhg","platform":"roblox","description":"CoffeeFolder and CoffeeBaseValue Class implementation for parity with most important methods/properties of Roblox' Folder and BaseValue Instances","version":"2.4.0","latest":"2.4.0","versions":["1.0.0","1.0.1","2.3.2","2.3.3","2.3.4","2.3.5","2.3.6","2.3.7","2.4.0"],"license":"Apache-2.0","licenseRating":"safe","licenseCaveats":["Modified files must carry a notice of changes. If the package ships a NOTICE file, its attributions must be preserved."],"licenseVerified":true,"dependencies":{"coffilhg/coffeeparser":{"version":"~1.0.0","alias":"CoffeeParser"},"data-oriented-house/lemonsignal":{"version":"^2.0.0","alias":"LemonSignal"}},"integrity":"a413357b749e85e5a7098e5d7fe8c71b90542814f8a0648dbf78f64847e64c2a","likes":0,"downloads":0,"install":"forest install coffilhg/coffeeobjects","url":"https://forest.dev/p/roblox/coffilhg/coffeeobjects","files":"https://api.forest.dev/ai/package/roblox/coffilhg/coffeeobjects/files","readme":"# ☕ CoffeeObjects\r\n\r\n**CoffeeObjects** is a lightweight, in-memory data tree that mimics Roblox’s\r\n`Folder` and `BaseValue` instances **without creating any Instances**.\r\n\r\nIt is designed for:\r\n\r\n* datastore / ProfileStore–style data\r\n* predictable change signals\r\n* zero Workspace or Instance overhead\r\n\r\nThis library intentionally favors **explicitness, performance, and Roblox semantics** over abstraction.\r\n\r\n---\r\n\r\n## Available Here!\r\n- **[This repository](src/init.luau) ~ [src/init.luau](src/init.luau)**\r\n- **[Wally](<https://wally.run/package/coffilhg/coffeeobjects>)**\r\n\r\n    ```toml\r\n    CoffeeObjects = \"coffilhg/coffeeobjects@2.4.0\"\r\n    ```\r\n- **[Rotriever](<https://github.com/Coffilhg/Useful-Modules/releases/tag/vCoffeeObjects/2.4.0>)**\r\n\r\n    ```toml\r\n    CoffeeObjects = \"github.com/Coffilhg/Useful-Modules@CoffeeObjects/2.4.0\"\r\n    ```\r\n<!-- **[Creator Store](<https://create.roblox.com/store/category/gameplay?creatorName=coffilhg>)** ~ **[CoffeeObjects](<https://create.roblox.com/store/asset/1234567890/CoffeeObjects>)**-->\r\n\r\n---\r\n\r\n## Features\r\n\r\n* `CoffeeFolder` - virtual equivalent of `Folder`\r\n* `CoffeeBaseValue` - virtual equivalent of `BaseValue`\r\n* Supports most Roblox primitive datatypes (`CFrame`, `Color3`, `Vector3`, etc.)\r\n* `ChildAdded`, `ChildRemoved`, `Destroying` and `Changed` signals (via GoodSignal by Stravant)\r\n* Automatic wrapping:\r\n\r\n  * primitives → `CoffeeBaseValue`\r\n  * tables → `CoffeeFolder`\r\n* Deterministic tree paths via `GetPath()` and `GetFullName()`\r\n* Deterministic recursive destruction\r\n\r\n---\r\n\r\n## Installation\r\n\r\n1. Copy the module into your project\r\n2. Include **GoodSignal**\r\n   [https://github.com/stravant/goodsignal](https://github.com/stravant/goodsignal)\r\n3. Require the module:\r\n\r\n```lua\r\nlocal CoffeeObjects = require(path.To.CoffeeObjects)\r\n```\r\n\r\n---\r\n\r\n## Basic Usage\r\n\r\n### Creating folders and values\r\n\r\n```lua\r\nlocal CoffeeFolder = CoffeeObjects.CoffeeFolder\r\nlocal CoffeeBaseValue = CoffeeObjects.CoffeeBaseValue\r\n\r\nlocal data = CoffeeFolder.new({\r\n\tStats = {\r\n\t\tHoney = 0,\r\n\t\tLevel = 5,\r\n\t},\r\n\tInventory = {\r\n\t\t\"Sword\",\r\n\t\t\"Shield\",\r\n\t},\r\n  Pets = {\r\n\t\t{\r\n\t\t\tName = \"Mark\",\r\n      Age = 1,\r\n      Species = \"Dog\",\r\n\t\t},\r\n\t}\r\n})\r\n```\r\n\r\nEverything is wrapped automatically.\r\n\r\n---\r\n\r\n### Reading & writing values\r\n\r\n```lua\r\nprint(data.Stats.Honey.Value) -- number: 0\r\n\r\ndata.Stats.Honey.Value = 10\r\n```\r\n\r\n### Listening for changes\r\n\r\n```lua\r\ndata.Stats.Honey.Changed:Connect(function(old, new)\r\n\tprint(old, \"→\", new)\r\nend)\r\n```\r\n\r\n### Mutable value changes are not detected\r\n\r\n**`.Changed` only fires when the `.Value` property is assigned. Mutating the object currently stored in `.Value` does not fire `.Changed`.**\r\n\r\nAssignments to `.Value` fire `.Changed`, including compound assignments such as `+=`, `*=`, `/=`, `-=`.\r\n\r\nYou can manually fire `.Changed` after mutating a value, but be aware that `old == new` may still be true if both arguments reference the same mutable object or the arguments you've passed to `.Changed:Fire(old, new)` are the same value.\r\n\r\n<details>\r\n\r\n<summary><strong>Example with buffer</strong></summary>\r\n\r\n```lua\r\nlocal function ReadBinary(bufferToRead: buffer, startBit: number, endBit: number): string\r\n\tif startBit == endBit then\r\n\t\treturn `{buffer.readbits(bufferToRead, startBit, 1)}`\r\n\tend\r\n\tif startBit > endBit then\r\n\t\tlocal originalEnd: number = endBit\r\n\t\tendBit = startBit\r\n\t\tstartBit = originalEnd\r\n\tend\r\n\r\n\tlocal result = {}\r\n\r\n\tfor i = startBit, endBit do\r\n\t\ttable.insert(result, buffer.readbits(bufferToRead, i, 1))\r\n\tend\r\n\r\n\treturn table.concat(result, \"\")\r\nend\r\n\r\nlocal function ReadAllBinary(bufferToRead: buffer): string\r\n\treturn ReadBinary(\r\n\t\tbufferToRead,\r\n\t\t0,\r\n\t\tbuffer.len(bufferToRead)*8 - 1 -- buffers are 0-indexed\r\n\t)\r\nend\r\n\r\n-- type is automatically CoffeeObjects.CoffeeBaseValueStrict<buffer>\r\n-- because we are using the strictNew\r\nlocal BufferValue = CoffeeBaseValue.strictNew(buffer.create(3))\r\n\r\nBufferValue.Changed:Connect(function(old, new)\r\n\tlocal isTheSame = old == new -- this will always be true with approach #1\r\n\tlocal oldBinary = ReadAllBinary(old)\r\n\tlocal newBinary = ReadAllBinary(new)\r\n\t\r\n\tprint(`{isTheSame}\\nOld: {oldBinary}\\n |\\n\\\\ /\\nNew: {newBinary}`)\r\nend)\r\n\r\n-- approach #1: you could change the buffer and Fire the signal!\r\nbuffer.writeu8(BufferValue.Value, 1, 255)\r\nBufferValue.Changed:Fire(BufferValue.Value, BufferValue.Value)\r\n\r\n-- approach #2: you could make a copy and change the .Value\r\nlocal old = BufferValue.Value\r\nlocal new = buffer.create(buffer.len(old))\r\nbuffer.copy(new, 0, old) -- make copy\r\nbuffer.writeu8(new, 2, 5) -- modify copy\r\n\r\nBufferValue.Value = new -- update\r\n```\r\n\r\nif you run this, you'll see the following output for both approaches\r\n```lua\r\n  true\r\nOld: 000000001111111100000000\r\n |\r\n\\ /\r\nNew: 000000001111111100000000\r\n  false\r\nOld: 000000001111111100000000\r\n |\r\n\\ /\r\nNew: 000000001111111110100000\r\n```\r\n\r\n</details>\r\n\r\n<details>\r\n\r\n<summary><strong>Example with CFrame</strong></summary>\r\n\r\n```lua\r\nlocal CFrameValue = CoffeeBaseValue.strictNew(CFrame.identity)\r\n\r\nCFrameValue.Changed:Connect(function(old, new)\r\n\tprint(\"Change detected:\", old, \"\\t->\\t\", new)\r\nend)\r\n\r\nCFrameValue.Value:Lerp(CFrame.new(1, 0, 1), 0.5) -- stays undetected\r\nprint(CFrameValue.Value) -- although the CFrame changes\r\n\r\n-- that is detected\r\nCFrameValue.Value = CFrameValue.Value:Lerp(CFrame.new(0, 0, 0), 0.5)\r\n```\r\n\r\n</details>\r\n\r\n<details>\r\n\r\n<summary><strong>Example with Vector3</strong></summary>\r\n\r\n```lua\r\nlocal Vector3Value = CoffeeBaseValue.strictNew(Vector3.zero)\r\n\r\nVector3Value.Changed:Connect(function(old, new)\r\n\tprint(\"Change detected:\", old, \"\\t->\\t\", new)\r\nend)\r\n\r\nVector3Value.Value += Vector3.new(1, 2, 3) -- detected --   Change detected: 0, 0, 0 \t->\t 1, 2, 3\r\nVector3Value.Value *= 3 -- detected --   Change detected: 1, 2, 3 \t->\t 3, 6, 9\r\nVector3Value.Value /= 2 -- detected --   Change detected: 3, 6, 9 \t->\t 1.5, 3, 4.5\r\nVector3Value.Value -= Vector3.new(5, 5, 5) -- detected --   Change detected: 1.5, 3, 4.5 \t->\t -3.5, -2, -0.5\r\n```\r\n\r\n</details>\r\n\r\n---\r\n\r\n## Internal Fields (`_` prefixed)\r\n\r\n> **The underscore (`_`) prefix is a convention, that such fields are private to the object itself and shall not be used by any other means.**\r\n\r\nFields prefixed with `_` are internal runtime state and are not part of the public API. Although you can use them, it is NOT recommended to - only use if you really know what you're doing!\r\n\r\nThe internal fields have an export type definition if you ever truly need those. Can be used via type intersections, e.g.: `CoffeeBaseValue & CoffeeBaseValueInternals<SupportedTypesList>`\r\n\r\n---\r\n\r\n### Child signals\r\n\r\n```lua\r\ndata.ChildAdded:Connect(function(key, child)\r\n\tprint(\"Added:\", key)\r\nend)\r\n\r\ndata.ChildRemoved:Connect(function(key)\r\n\tprint(\"Removed:\", key)\r\nend)\r\n```\r\n\r\n---\r\n\r\n## Paths\r\n\r\nEvery object knows where it lives in the tree:\r\n\r\n```lua\r\nprint(data.Stats.Honey:GetPath()) -- { \"Stats\", \"Honey\" }\r\nprint(data.Stats.Honey:GetFullName()) -- Stats.Honey\r\nprint(data.Stats.Honey:GetFullName(\"/\")) -- Stats/Honey\r\n\r\nprint(data.Pets[1].Name:GetPath()) -- { \"Pets\", 1, \"Name\" }\r\nprint(data.Pets[1].Name:GetFullName()) -- Pets.1.Name\r\nprint(data.Pets[1].Name:GetFullName(\"/\")) -- Pets/1/Name\r\n```\r\n\r\nPaths are reconstructed via parent references - no global registry.\r\n\r\nWhen making a DeepCopy(), make sure internal \"_parent\" references are not copied when copying a CoffeeBaseValue or CoffeeFolder.\r\n\r\nUse `.validateClass(CoffeeObject)` and `validateUnlinkedClass(CoffeeObject)` methods to check for CoffeeBaseValue or CoffeeFolder whenever you encounter a value whose `type()` is `\"table\"`!\r\n\r\n---\r\n\r\n## Arrays vs Dictionaries\r\n\r\n`CoffeeFolder` distinguishes **array-like** folders from dictionaries using Roblox semantics:\r\n\r\n```lua\r\nprint(#data.Inventory, data.Inventory:_IsArrayORTuple())\r\n-- 2, true\r\n\r\nprint(#data.Stats, data.Stats:_IsArrayORTuple())\r\n-- 0, false\r\n```\r\n\r\n### Inserting into arrays\r\n\r\n```lua\r\ndata.Inventory:Insert(\"Potion\")\r\n```\r\n\r\nAttempting to insert into a dictionary will warn and do nothing.\r\n\r\n---\r\n\r\n## ⚠️ Important Behavior Notes\r\n\r\n### Overwriting keys now ALWAYS fires signals by default\r\n\r\nWhen you overwrite an existing key or index in a `CoffeeFolder`:\r\n\r\n```lua\r\ndata.Stats.Honey = 25\r\n```\r\n\r\n* First the `ChildRemoved` **fires**\r\n* Then the `ChildAdded` **fires**\r\n* The old object **is destroyed**\r\n\r\nThis behavior is **intentional and NOT configurable**.\r\n**If you want to avoid such behavior**, do this:\r\n\r\n```lua\r\n-- instead of overwriting the index as in example above (data.Stats.Honey = 25)\r\n-- use the API!\r\ndata.Stats.Honey.Value = 25\r\n```\r\n\r\n---\r\n\r\n### `validateUnlinkedClass` is intentionally permissive\r\n\r\nFunctions like:\r\n\r\n```lua\r\nCoffeeBaseValue.validateUnlinkedClass(v)\r\nCoffeeFolder.validateUnlinkedClass(v)\r\n```\r\n\r\nexist to support:\r\n\r\n* deep copies\r\n* reconciliation\r\n* lost metatables\r\n\r\n⚠️ **They are easy to spoof by design.**\r\n\r\nIf you want stricter validation, you can add a marker:\r\n\r\n```lua\r\nrawset(self, \"__coffee\", \"BaseValue\")\r\n-- or\r\nrawset(self, \"__coffee\", \"Folder\")\r\n```\r\n\r\n...and update `validateUnlinkedClass` accordingly.\r\n\r\nThis is left to the consumer on purpose to avoid opinionated constraints.\r\n\r\n### Internal `_Destroying` signal\r\n\r\nCoffeeObjects now uses (Folders now listen to) an internal `_Destroying` signal to ensure safe unlinking\r\nfrom parent structures before the public `Destroying` signal fires.\r\n\r\nThis guarantees:\r\n- no stale references in `CoffeeFolder` if `:DisconnectAll()` was called on `Destroying`\r\n- safe `:DisconnectAll()` behavior\r\n- consistent destruction ordering\r\n\r\n---\r\n\r\n## Destruction\r\n\r\nDestroying a folder:\r\n\r\n* `_Destroying` fires before `Destroying` (internal use for safe unlinking)\r\n* disconnects all signals\r\n* destroys all children recursively\r\n* clears parent links\r\n* removes the metatable\r\n\r\n```lua\r\ndata:Destroy()\r\n```\r\n\r\nAfter destruction, the object is inert.\r\n\r\nIf you understand Roblox' `Folder` and `BaseValue`, you already understand this library.\r\n\r\n---\r\n\r\n## DEPENDENCIES\r\n\r\n- **[LemonSignal](<https://github.com/Data-Oriented-House/LemonSignal>)**\r\n- **[CoffeeParser](<https://github.com/Coffilhg/Useful-Modules/tree/CoffeeParser>)**\r\n\r\n---\r\n\r\n## 📜 License & Attribution\r\n\r\nThis project is licensed under **Apache License 2.0**.\r\n\r\nSee the full terms in the [LICENSE](LICENSE) file.\r\n\r\nAttribution is preserved in the [NOTICE](NOTICE) file.\r\n\r\nCopyright © 2025 @Coffilhg (Roblox UserId 517222346)","readmeTruncated":false}