{"id":"pushvs/vide-charm","name":"vide-charm","scope":"pushvs","platform":"roblox","description":"Mirrored from the Wally registry.","version":"0.0.1","latest":"0.0.1","versions":["0.0.1"],"license":"MIT","licenseRating":"safe","licenseCaveats":["License identified from the packaged LICENSE file; the manifest declared none."],"licenseVerified":true,"dependencies":{"littensy/charm":{"version":"^0.11.0","alias":"Charm"},"pushvs/vide":{"version":"^0.0.7","alias":"Vide"}},"integrity":"b2167293f01b06370c423aa2146e441b15e888ce936d6547bfb305bdaeb63296","likes":0,"downloads":0,"install":"forest install pushvs/vide-charm","url":"https://forest.dev/p/roblox/pushvs/vide-charm","files":"https://api.forest.dev/ai/package/roblox/pushvs/vide-charm/files","readme":"<p align=\"center\">\n  <p align=\"center\">\n\t<img width=\"150\" height=\"150\" src=\"https://raw.githubusercontent.com/littensy/charm/main/assets/logo.png\" alt=\"Logo\">\n  </p>\n  <h1 align=\"center\"><b>Charm</b></h1>\n  <p align=\"center\">\n    Fine-grained reactivity for Roblox\n    <br />\n    <a href=\"https://npmjs.com/package/@rbxts/charm\"><strong>npm package →</strong></a>\n  </p>\n</p>\n\n<div align=\"center\">\n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/littensy/charm/ci.yml?style=for-the-badge&branch=main&logo=github)\n[![NPM Version](https://img.shields.io/npm/v/@rbxts/charm.svg?style=for-the-badge&logo=npm)](https://www.npmjs.com/package/@rbxts/charm)\n[![GitHub License](https://img.shields.io/github/license/littensy/charm?style=for-the-badge)](LICENSE)\n\n</div>\n\nCharm is a reactive state management library designed for storing data in discrete containers called _signals_. Connect behavior to data with signals, ensuring systems stay up-to-date with their underlying data while reactivity eliminates the need for manual updates.\n\n**Build game state from simple building blocks:**\n\n- Store data in [signals](#signalinitialvalue-equals): reactive state containers that hold a value\n- React to state changes with [effects](#effectcallback): re-run code when signals update\n- Derive new values from data with [computed signals](#computedgetter): memoize functions that access signals\n\n**Want to learn more about reactivity?**\n\n- https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf\n- https://preactjs.com/blog/introducing-signals\n- https://docs.solidjs.com/advanced-concepts/fine-grained-reactivity\n- https://github.com/roblox/signals\n\n[Migrating from an older version of Charm?](#migration)\n\n<details>\n<summary><b>Table of Contents</b></summary>\n\n- [Installation](#installation)\n- [Reference](#reference)\n    - [`signal(initialValue, equals?)`](#signalinitialvalue-equals)\n    - [`computed(getter)`](#computedgetter)\n    - [`effect(callback)`](#effectcallback)\n    - [Nested effects](#nested-effects)\n    - [`effectScope(callback)`](#effectscopecallback)\n    - [`listen(getter, callback)`](#listengetter-callback)\n    - [Getter functions](#getter-functions)\n    - [`observe(getter, callback)`](#observegetter-callback)\n    - [`subscribe(getter, callback)`](#subscribegetter-callback)\n    - [`untracked(callback)`](#untrackedcallback)\n    - [`batch(callback)`](#batchcallback)\n    - [`mapped(getter, transform)`](#mappedgetter-transform)\n    - [`onCleanup(callback, failSilently?)`](#oncleanupcallback-failsilently)\n    - [`atom(initialValue, equals?)`](#atominitialvalue-equals)\n    - [`trigger(callback)`](#triggercallback)\n    - [`flags`](#flags)\n- [Client-Server Sync](#client-server-sync)\n    - [Installation](#installation-1)\n    - [Quick Start](#quick-start)\n    - [Server API](#config)\n    - [Client API](#clientaddsignalssetters)\n    - [Sync Caveats](#sync-caveats)\n- [Deep Reactivity](#deep-reactivity)\n    - [Installation](#installation-2)\n    - [`reactive(initialValue)`](#reactiveinitialvalue)\n    - [Mutation vs. update function](#mutation-vs-update-function)\n    - [`toRaw(value)`](#torawvalue)\n    - [`isReactive(value)`](#isreactivevalue)\n- [Migration](#migration)\n- [Examples](#examples)\n\n</details>\n\n## At a Glance\n\n```luau\nlocal getTodos, setTodos = signal({} :: { string })\nlocal getQuery, setQuery = signal(\"\")\n\nobserve(getTodos, function(todo: string, index: number)\n\tlocal instance = Instance.new(\"TextLabel\")\n\n\tlocal getText = computed(function()\n\t\treturn getTodos()[index] or \"\"\n\tend)\n\n\teffect(function()\n\t\tinstance.Text = getText()\n\t\tinstance.Visible = string.match(getText(), getQuery()) ~= nil\n\tend)\n\n\tinstance.LayoutOrder = index\n\tinstance.Size = UDim2.new(1, 0, 0, 40)\n\tinstance.Parent = screenGui\n\n\treturn function()\n\t\tinstance:Destroy()\n\tend\nend)\n\nsetTodos({ \"Buy milk\", \"Buy eggs\", \"Play Roblox\" })\nsetQuery(\"Buy\")\n```\n\n<details>\n<summary>Explain code</summary>\n\n```luau\n-- Declare state for a todo list and a search query\nlocal getTodos, setTodos = signal({} :: { string })\nlocal getQuery, setQuery = signal(\"\")\n\n-- Create a text label when an item is added to the list\nobserve(getTodos, function(todo, index)\n\tlocal instance = Instance.new(\"TextLabel\")\n\n\t-- Create a memoized function that only re-runs effects when the todo list\n\t-- updates or the function returns new text\n\tlocal getText = computed(function()\n\t\treturn getTodos()[index] or \"\"\n\tend)\n\n\t-- Update instance properties when the text or query updates\n\teffect(function()\n\t\tinstance.Text = getText()\n\t\tinstance.Visible = string.match(getText(), getQuery()) ~= nil\n\tend)\n\n\tinstance.LayoutOrder = index\n\tinstance.Size = UDim2.new(1, 0, 0, 40)\n\tinstance.Parent = screenGui\n\n\t-- Destroy the instance when this item is removed\n\treturn function()\n\t\tinstance:Destroy()\n\tend\nend)\n\n-- Add items to the todo list\nsetTodos({ \"Buy milk\", \"Buy eggs\", \"Play Roblox\" })\n-- Filter for items containing \"Buy\"\nsetQuery(\"Buy\")\n```\n\n</details>\n\n## Installation\n\n```sh\nnpm install @rbxts/charm\nyarn add @rbxts/charm\npnpm add @rbxts/charm\n```\n\n```toml\n# wally.toml\n[dependencies]\nCharm = \"littensy/charm@VERSION\"\n```\n\n---\n\n## Reference\n\n### `signal(initialValue, equals?)`\n\nSignals are the core of reactivity in Charm. The `signal` function creates a reactive signal that acts as a container for a value. It returns a function to access the value, and another to update the value.\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\nsetCounter(1)\nsetCounter(function(count)\n\treturn count + 1\nend)\nprint(getCounter()) -- 2\n```\n\nAccessing the signal's value in an effect or computed signal will subscribe to it as a dependency. Changing the value will immediately notify every effect and computed signal that depends on the signal, ensuring all of your state is correct and up-to-date.\n\nYou can also pass a custom equality function to only update the signal if the equality function returns `false`:\n\n```luau\nlocal getMax, setMax = signal(0, function(current, incoming)\n\treturn incoming <= current\nend)\n\nsetMax(1) -- 1\nsetMax(2) -- 2\nsetMax(-1) -- 2\n```\n\n> [!NOTE]\n> Looking for atoms? You can still use [`atom()`](#atominitialvalue-equals) to create a signal with a unified getter and setter.\n\n---\n\n### `effect(callback)`\n\nEffects are fundamental to reactivity, allowing you to react to signal updates. The `effect` function subscribes to signals accessed by the effect callback, and when a dependency updates, the callback will re-execute.\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\neffect(function()\n\tprint(`Count is {getCounter()}`)\nend) -- Count is 0\n\nsetCounter(1) -- Count is 1\n```\n\nYou can also return a cleanup function that will run once, either before the effect re-runs or when the effect is disposed:\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\nlocal dispose = effect(function()\n\tlocal count = getCounter()\n\treturn function()\n\t\tprint(`Cleanup {count}`)\n\tend\nend)\n\nsetCounter(1) -- Cleanup 0\ndispose() -- Cleanup 1\n```\n\n### Nested Effects\n\nAn effect is _nested_ if it was created during the execution of another effect. In Charm, when an effect with nested effects re-runs or gets cleaned up, the nested effects from the previous run are automatically cleaned up and re-created if needed. This prevents memory leaks and ensures that outer effects always run before their inner effects:\n\n```luau\nlocal getPrintCount, setPrintCount = signal(true)\nlocal getCount, setCount = signal(1)\n\neffect(function()\n\tif getPrintCount() then\n\t\t-- This inner effect is created when getPrintCount() is true\n\t\teffect(function()\n\t\t\tprint(`Count is {getCount()}`)\n\t\tend)\n\tend\nend) -- Count is 1\n\nsetCount(2) -- Count is 2\n\n-- This re-runs the outer effect and cleans up old inner effects\nsetPrintCount(false)\n\nsetCount(3) -- No output\n```\n\n> [!NOTE]\n> To run code that is \"detached\" from the parent effect or scope, use `untracked()` or a detached effect scope. If you suspect that the new nested effect behavior is causing issues with migration, try disabling the `flags.trackInnerEffects` flag to assist with debugging.\n\n---\n\n### `computed(getter)`\n\nThe `computed` function creates a read-only signal whose value is derived from other signals. The computed signal caches the getter function's last result, and the value is only re-computed if a dependency has updated since the last computation.\n\n```luau\nlocal getName, setName = signal(\"John\")\nlocal getSurname, setSurname = signal(\"Doe\")\nlocal getFullName = computed(function()\n\treturn `{getName()} {getSurname()}`\nend)\n\nprint(getFullName()) -- \"John Doe\"\nsetName(\"Jane\")\nprint(getFullName()) -- \"Jane Doe\"\n```\n\nThe getter function also receives the previous result (or `nil` during the initial run). You can use this for computed signals that depend on the previous result:\n\n```luau\nlocal getCounter, setCounter = signal(10)\nlocal getMax = computed(function(prevMax)\n\treturn math.max(getCounter(), prevMax or 0)\nend)\n\nprint(getMax()) -- 10\nsetCounter(5)\nprint(getMax()) -- 10\n```\n\n---\n\n### `effectScope(callback)`\n\nScopes allow you to dispose multiple effects at once. The `effectScope` function creates a scope that tracks inner effects, so effects created during the execution of the callback will clean up when the scope disposes.\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\nlocal dispose = effectScope(function()\n\teffect(function()\n\t\tprint(`Count 1 is {getCounter()}`)\n\tend)\n\teffect(function()\n\t\tprint(`Count 2 is {getCounter()}`)\n\tend)\nend)\n\nsetCounter(1) -- Count 1 is 1, Count 2 is 1\ndispose()\nsetCounter(2) -- No output; effects got disposed\n```\n\nSimilar to `effect()`, the callback can return a cleanup function that runs when the effect scope is disposed.\n\n---\n\n### `listen(getter, callback)`\n\nThe `listen` function creates an effect that only subscribes to the signals accessed by `getter`. Signals accessed by the callback will not be subscribed to, avoiding accidental subscriptions when you want to run side effects.\n\nThe callback also receives the previous value, or `nil` when running for the first time.\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\nlisten(getCounter, function(count, prevCount)\n\tprint(`Count is {count} (was {prevCount})`)\nend) -- Count is 0 (was nil)\n\nsetCounter(1) -- Count is 1 (was 0)\n```\n\nNote that the listener callback runs in `untracked()`, so nested effects are not cleaned up.\n\n### Getter functions\n\nIn most Charm APIs, you can also subscribe to getter functions that call one or more signals, and they will automatically be tracked:\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\nlocal function floorCounter()\n\treturn math.floor(getCounter())\nend\n\nlisten(floorCounter, function(count, prevCount)\n\tprint(`Floor of count is {count} (was {prevCount})`)\nend) -- Floor of count is 0 (was nil)\n\nsetCounter(0.5) -- Doesn't print anything, floor is still 0\nsetCounter(1) -- Floor of count is 1 (was 0)\n```\n\n---\n\n### `observe(getter, callback)`\n\n[Observers](https://sleitnick.github.io/RbxObservers/docs/observer-pattern) allow you to track the lifetime of a given state. The `observe` function executes the callback for every unique key added to a table, and disposes the callback when that key is removed.\n\n```luau\nlocal getItems, setItems = signal({ a = 0, b = 0 })\n\nobserve(getItems, function(value, key)\n\tprint(`Added {key}`)\n\treturn function()\n\t\tprint(`Removed {key}`)\n\tend\nend) -- Added a, Added b\n\nsetItems({ a = 0, c = 0 }) -- Removed b, Added c\n```\n\nThe callback runs in an effect scope, so effects created in the callback will be disposed when the key is removed:\n\n```luau\nlocal getItems, setItems = signal({})\n\nlocal dispose = observe(getItems, function(value, key)\n\tlocal getValue = computed(function(prevValue)\n\t\treturn getItems()[key] or prevValue\n\tend)\n\n\teffect(function()\n\t\tlocal value = getValue()\n\t\tprint(`Set {key} = {value}`)\n\t\treturn function()\n\t\t\tprint(`Cleanup {key} = {value}`)\n\t\tend\n\tend)\nend)\n\nsetItems({ a = 0, b = 0 }) -- Set a = 0, Set b = 0\nsetItems({ a = 1, b = 0 }) -- Cleanup a = 0, Set a = 1\nsetItems({ a = 1 }) -- Cleanup b = 0\ndispose() -- Cleanup a = 1\n```\n\n---\n\n### `subscribe(getter, callback)`\n\nThe `subscribe` function is identical to `listen()`, but the callback does not run initially. The callback only runs when the value returned by the getter function changes.\n\n```luau\nlocal getCounter, setCounter = signal(0)\n\n-- Does not output anything initially\nsubscribe(getCounter, function(count, prevCount)\n\tprint(`Count is {count} (was {prevCount})`)\nend)\n\nsetCounter(1) -- Count is 1 (was 0)\n```\n\n---\n\n### `untracked(callback)`\n\nIn case you want to opt-out of dependency tracking in an effect, you can use `untracked()` to call a function _outside_ the current scope, preventing signals and effects in the function from being tracked.\n\n```luau\nlocal getTracked, setTracked = signal(0)\nlocal getUntracked, setUntracked = signal(0)\n\neffect(function()\n\tprint(`Tracked: {getTracked()}, Untracked: {untracked(getUntracked)}`)\nend) -- Tracked: 0, Untracked: 0\n\nsetTracked(1) -- Tracked: 1, Untracked: 0\nsetUntracked(1) -- No output\nsetTracked(2) -- Tracked: 2, Untracked: 1\n```\n\nBecause `untracked()` executes the callback outside the current effect, nested effects created during the execution of the callback will not be tracked by the parent effect:\n\n```luau\nlocal stopEffect\nlocal stopScope = effectScope(function()\n\tuntracked(function()\n\t\tstopEffect = effect(function()\n\t\t\treturn function()\n\t\t\t\tprint(\"Cleaned up untracked effect\")\n\t\t\tend\n\t\tend)\n\tend)\nend)\n\nstopScope() -- No output, the scope did not track the effect\nstopEffect() -- Cleaned up untracked effect\n```\n\n---\n\n### `batch(callback)`\n\nCombines multiple signal updates made by the callback into a single commit that triggers effects once the callback completes.\n\n```luau\nlocal getName, setName = signal(\"John\")\nlocal getSurname, setSurname = signal(\"Doe\")\n\neffect(function()\n\tprint(`Full name: {getName()} {getSurname()}`)\nend)\n\n-- Combines both writes into a single update.\n-- Once the callback completes, outputs \"Full name: Foo Bar\"\nbatch(function()\n\tsetName(\"Foo\")\n\tsetSurname(\"Bar\")\nend)\n```\n\n---\n\n### `mapped(getter, transform)`\n\nThe `mapped` function iterates over every key in a table and uses the transform function to assign them to a new key and value. The result is returned as a read-only signal containing the new keys and values. When a key's value changes, or a new key is added to the table, `transform` is called for that key and its current value.\n\nThe first value returned by the transform function is used as the new value:\n\n```luau\nlocal getList, setList = signal({ \"a\", \"b\", \"c\" })\n\nlocal getUppercase = mapped(getList, function(value)\n\treturn string.upper(value)\nend)\n\nprint(getUppercase()) -- { \"A\", \"B\", \"C\" }\n```\n\nIf the transform function returns two values, the second value is used as the new key:\n\n```luau\nlocal getList, setList = signal({ \"a\", \"b\", \"c\" })\n\nlocal getSwapped = mapped(getList, function(value, key)\n\treturn key, value\nend)\n\nprint(getSwapped()) -- { a = 1, b = 2, c = 3 }\n```\n\n---\n\n### `onCleanup(callback, failSilently?)`\n\nThe `onCleanup` function binds the callback to the currently running effect or effect scope. Multiple cleanup functions can be bound to the same effect.\n\nUnless `failSilently` is set to `true`, this function will emit a warning if there is no active effect or scope.\n\n```luau\nlocal dispose = effectScope(function()\n\tonCleanup(function()\n\t\tprint(\"Cleaned up\")\n\tend)\nend)\n\ndispose() -- Cleaned up\n```\n\n---\n\n### `atom(initialValue, equals?)`\n\nThe `atom` function creates a new reactive signal and returns a single function that acts as both a getter and setter.\n\nIf the atom is called with 0 arguments, the atom returns the current value and subscribes to the signal. Otherwise, when called with 1 or more arguments, the atom will update the signal's value.\n\n```luau\nlocal counter = atom(0)\n\nprint(counter()) -- 0\ncounter(1)\ncounter(function(count)\n\treturn count + 1\nend)\n```\n\nYou can also pass a custom equality function to only update the signal if the new value is _not_ equal to the cu","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/pushvs/roblox/vide-charm/0.0.1/readme"}