{"id":30473899,"url":"https://github.com/danielinoa/ripple","last_synced_at":"2025-09-11T02:39:18.753Z","repository":{"id":308261024,"uuid":"1032162133","full_name":"danielinoa/Ripple","owner":"danielinoa","description":"A tiny dependency-tracked reactivity runtime for Swift","archived":false,"fork":false,"pushed_at":"2025-08-05T21:42:31.000Z","size":50,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-01T04:30:05.382Z","etag":null,"topics":["atom","reactivity","signals","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danielinoa.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-04T23:00:42.000Z","updated_at":"2025-08-05T21:42:34.000Z","dependencies_parsed_at":"2025-08-05T02:41:07.512Z","dependency_job_id":null,"html_url":"https://github.com/danielinoa/Ripple","commit_stats":null,"previous_names":["danielinoa/ripple"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/danielinoa/Ripple","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielinoa%2FRipple","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielinoa%2FRipple/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielinoa%2FRipple/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielinoa%2FRipple/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danielinoa","download_url":"https://codeload.github.com/danielinoa/Ripple/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danielinoa%2FRipple/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274568584,"owners_count":25309282,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-11T02:00:13.660Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["atom","reactivity","signals","swift"],"created_at":"2025-08-24T09:50:53.086Z","updated_at":"2025-09-11T02:39:18.746Z","avatar_url":"https://github.com/danielinoa.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# Ripple\n\nA reactivity runtime for Swift that automatically tracks state, and **re-runs** dependents when state changes.\n\nRipple is designed to run on the **main actor**.\n\n```swift\nimport Ripple\n\n@Atom var count = 0\n@Derived var title = \"Count: \\(count)\"\n\nlet render = Effect {     // → keep the Effect alive\n    label.text = title    // → auto-updates when count changes\n}\n\ncount += 1                // → title recomputes → effect runs\n```\n\n## Outline\n- [Features](#features)\n- [Installation](#installation)\n    - [Swift Package Manager](#swift-package-manager)\n- [Concepts](#concepts)\n    - [Convenience forms](#convenience-forms)\n- [Testing \u0026 isolation](#testing--isolation)\n- [Behavior details](#behavior-details)\n- [Examples](#examples)\n    - [UIKit binding](#uikit-binding)\n    - [Chained derived values](#chained-derived-values)\n    - [Parallel-safe tests](#parallel-safe-tests)\n- [Roadmap](#roadmap)\n- [License](#license)\n\n## Features\n* **Ergonomic API**: `@Atom var count = 0`, `@Derived var title = \"Count: \\(count)\"`.\n* **Fine-grained tracking**: only recompute what actually depends on what changed.\n* **Memoized computed values**: `@Derived` caches and invalidates on upstream mutations.\n* **Side effects**: `Effect { … }` runs once up-front and whenever its inputs change.\n* **Test isolation**: `withIsolatedRuntime { … }` gives each test a fresh graph.\n* **No macros, no Combine**: simple value semantics and identity-based graphs.\n\n## Installation\n\n### Swift Package Manager\nAdd the package in Xcode (**File → Add Packages…**) or declare it in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/danielinoa/Ripple.git\", branch: \"main\")\n```\n\n## Concepts\n\n### `@Atom`: The fundamental “state” type.\n```swift\n@Atom var name = \"Ripple\"\nname = \"Ripple 2\"           // notifies dependents if value actually changed\nprint($name)                // `$` exposes the AtomObject\n```\n* Reads link the current subscriber.\n* Writes call `Runtime.didMutate` **only if the value changes** (`Equatable`).\n\n### `@Derived`: A **cached, read-only value** that is *derived* from other state.\n```swift\n@Atom var x = 1\n@Atom var y = 2\n@Derived var sum = x + y     // cached until x or y mutates\n```\nIf `T : Equatable`, Ripple skips propagation when the recomputed value equals the cached one.\n\n### `Effect`: An observer that fires when its enclosed dependencies change.\n```swift\n@Atom var count = 0\nvar bag: [Effect] = []\n\nbag.append(Effect {\n    label.text = \"Count: \\(count)\"\n})\n```\nRuns once on creation and after every relevant mutation.\n\n### Convenience forms\nThe property‑wrapper syntax is most succinct, but Ripple also exposes\nlow‑level factory helpers that return the underlying nodes. This is useful when you\nneed explicit type annotations or want to store the nodes in collections.\n\n| Purpose | Property‑wrapper | Factory function |\n|---------|------------------|------------------|\n| Mutable state | `@Atom var count = 0` | `let count = atom(0)` |\n| Cached value  | `@Derived var total = price * qty` | `let total: Derivation\u003cInt\u003e = derive { price * qty }` |\n\nBoth forms interoperate:\n\n```swift\n@Atom var price = 10\nlet qty = atom(2)\n\n@Derived var total = price * qty.value // wrapper + node\nlet tax = derive { total + 1 } // node based on wrapper\n\nprint(tax.value) // 21\n```\n\nChoose whichever style reads best in a given context; under the hood they\nall participate in the same dependency graph.\n\n## Testing \u0026 isolation\n\n```swift\n@Test\nfunc example() {\n    withIsolatedRuntime {\n        @Atom var a = 1\n        @Derived var doubled = a * 2\n        #expect(doubled == 2)\n    }\n}\n```\n`withIsolatedRuntime` (sync \u0026 async overloads) swaps `Runtime.current` with a fresh graph for the duration of the task tree.\n\n## Behavior details\n| Item | Behaviour |\n|------|-----------|\n| **Atom writes** | Notify dependents only when `newValue != oldValue`. |\n| **Derived cache** | Recompute on first read or after any dependency mutates. |\n| **Derived equality** | If `T : Equatable`, skip propagation when value is unchanged. |\n| **Effect lifetime** | Runs while at least one strong reference exists; unsubscribes on deinit. |\n\n## Examples\n\n### UIKit binding\n```swift\nfinal class CounterVC: UIViewController {\n    private var label: UILabel = .init()\n    private var bag: [Effect] = []\n\n    @Atom\n    private var count = 0\n    \n    @Derived\n    private var title = \"Count: \\(count)\"\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        bag.append(Effect { self.label.text = self.title })\n    }\n\n    private func plus() { count += 1 }\n}\n```\n\n### Chained derived values\n```swift\n@Atom var a = 1\n@Derived var d1 = a * 2          // 2\n@Derived var d2 = d1 + 3         // 5\n@Derived var d3 = d2 * 4         // 20\nlet e = Effect { print(d3) }     // prints 20, then 36 when a = 3\n```\n\n### Parallel-safe tests\n```swift\n@Test\nfunc isolatedGraphs() async {\n  let r1: Int = await withIsolatedRuntime {\n    @Atom var x = 1 \n    @Atom var y = 2\n    @Derived var s = x + y\n    return s\n  }\n  let r2: Int = await withIsolatedRuntime {\n    @Atom var x = 10\n    @Atom var y = 20\n    @Derived var s = x + y\n    return s\n  }\n  #expect(r1 == 3 \u0026\u0026 r2 == 30)\n}\n```\n\n## Roadmap\n* **Scheduler**: coalesce burst mutations into a single run loop pass.\n\n## License\n\n### MIT License\n\nCopyright (c) 2025 Daniel Inoa\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the “Software”), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielinoa%2Fripple","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanielinoa%2Fripple","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanielinoa%2Fripple/lists"}