{"id":30899980,"url":"https://github.com/chickensoft-games/sync","last_synced_at":"2026-04-17T11:01:44.103Z","repository":{"id":313700247,"uuid":"1052312145","full_name":"chickensoft-games/Sync","owner":"chickensoft-games","description":"Simple, synchronous, single-threaded reactive programming primitives and collections with fluent bindings. Sync guarantees deterministic execution and defers mutations when executing bindings, protecting your code from reentrancy issues.","archived":false,"fork":false,"pushed_at":"2025-12-14T01:12:52.000Z","size":141,"stargazers_count":22,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-02-17T12:30:52.826Z","etag":null,"topics":["csharp","reactive","reactivex","synchronous"],"latest_commit_sha":null,"homepage":"https://www.nuget.org/packages/Chickensoft.Sync","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chickensoft-games.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","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,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-07T20:42:15.000Z","updated_at":"2025-12-14T06:18:06.000Z","dependencies_parsed_at":null,"dependency_job_id":"13a7eff8-ee42-4e4b-9beb-94f1bc80bb28","html_url":"https://github.com/chickensoft-games/Sync","commit_stats":null,"previous_names":["chickensoft-games/sync"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/chickensoft-games/Sync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chickensoft-games","download_url":"https://codeload.github.com/chickensoft-games/Sync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chickensoft-games%2FSync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31926260,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-17T10:35:34.458Z","status":"ssl_error","status_checked_at":"2026-04-17T10:35:09.472Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["csharp","reactive","reactivex","synchronous"],"created_at":"2025-09-09T04:53:42.125Z","updated_at":"2026-04-17T11:01:44.090Z","avatar_url":"https://github.com/chickensoft-games.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⚡️ Sync\n\n[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] [![Read the docs][read-the-docs-badge]][docs] ![line coverage][line-coverage] ![branch coverage][branch-coverage]\n\nSimple, synchronous, single-threaded reactive programming primitives and collections with fluent bindings. Sync guarantees deterministic execution and defers mutations when executing bindings, protecting your code from [reentrancy] issues.\n\n---\n\n\u003cp align=\"center\"\u003e\n\u003cimg alt=\"Chickensoft.Sync\" src=\"Chickensoft.Sync/icon.png\" width=\"200\"\u003e\n\u003c/p\u003e\n\n---\n\nSync enforces correctness by default, minimizes memory allocations, and simplifies creating new reactive primitives composed of atomic operations.\n\nSync is a C# library that works everywhere `netstandard2.1` works.\n\n## ⭐️ Features\n\n- ✅ Simplified terminology tailored for game development use cases.\n- ✅ Avoids boxing value types and minimizes heap allocations to reduce garbage collector pressure (suitable for games).\n- ✅ Includes observable collections such as `AutoList\u003cT\u003e`, `AutoSet\u003cT\u003e`, and `AutoMap\u003cTKey, TValue\u003e` which are built on top of .NET's standard collection types.\n- ✅ Provides an observable property/value (or `BehaviorSubject` in [ReactiveX](https://reactivex.io/documentation/subject.html) terminology) called `AutoValue\u003cT\u003e`.\n- ✅ Errors stop execution immediately, same as ordinary C# code.\n- ✅ Consistent, fluent bindings tailored for each reactive primitive.\n- ✅ Dispose of bindings to unsubscribe from notifications.\n- 🤩 *Easily build your own synchronous reactive primitives and collections composed of atomic operations* and notify listeners without having to worry about [reentrancy].\n\n\u003e [!TIP]\n\u003e Reactive primitives are synchronous event loops which use a few tricks to essentially eliminate heap allocations in performance critical hot paths.\n\n## 📖 Example Usage\n\nHere's a very simple, real-world game development example that shows how to idiomatically use Sync's `AutoValue\u003cT\u003e` to synchronize an Enemy's visual representation with its underlying model.\n\n\u003e [!NOTE]\n\u003e The `AutoValue\u003cint\u003e` and the binding to it `AutoValue\u003cint\u003e.Binding` need to be cleaned up when you're finished to avoid memory leaks.\n\n```csharp\n// Enemy gameplay logic\npublic sealed class Enemy : IDisposable\n{\n  // mutable observable value private to this class\n  private readonly AutoValue\u003cint\u003e _health = new(100);\n\n  // immutable view of the value for outside subscribers\n  public IAutoValue\u003cint\u003e Health =\u003e _health;\n\n  public void TakeDamage(int damage)\n  {\n    // enemy can't take more damage than it has health\n    var appliedDamage = Math.Min(Math.Abs(damage), _health.Value);\n    // bindings will be notified when this goes into effect\n    _health.Value -= appliedDamage;\n  }\n\n  public void Dispose()\n  {\n    // release references to any bindings to the health value so they can be GC'd\n    _health.Dispose();\n  }\n}\n\n// Enemy visualization logic\npublic sealed class EnemyView : IDisposable\n{\n  public Enemy Enemy { get; }\n  public AutoValue\u003cint\u003e.Binding Binding { get; }\n\n  public EnemyView(Enemy enemy)\n  {\n    Enemy = enemy;\n    // listen to changes in the enemy's health\n    Binding = enemy.Health.Bind();\n    Binding.OnValue(OnHealthChanged);\n  }\n\n  public void OnHealthChanged(int health)\n  {\n    // update the health bar UI, etc.\n  }\n\n  public void Dispose()\n  {\n    Binding.Dispose(); // stop listening\n  }\n}\n```\n\n\u003e [!TIP]\n\u003e By convention, objects which own the reactive primitive — the `_health` field in this example — retain a reference to the primitive itself and expose it publicly as a read-only reference that can be used to bind to it.\n\u003e\n\u003e ```csharp\n\u003e private readonly AutoValue\u003cint\u003e _health = new(100); // private mutable view\n\u003e public IAutoValue\u003cint\u003e Health =\u003e _health; // public read-only view\n\u003e ```\n\nSync has a few more features — we'll document the available APIs along with tips and tricks below.\n\n## 📦 Installation\n\nSync is available on [nuget].\n\n```sh\ndotnet add package Chickensoft.Sync\n```\n\n## 🔂 AutoValue\n\n`AutoValue\u003cT\u003e` stores a single value and will broadcast it immediately to any binding callbacks at registration to keep them synchronized. Bindings are notified of any changes to the value for as long as they remain subscribed.\n\n```csharp\n// hang onto the value for as long as you want to change it, then call Dispose()\n// when you're done with it\nvar autoValue = new AutoValue\u003cAnimal\u003e(new Cat(\"Pickles\"));\n\n// hang onto the binding for as long as you want to observe, then call Dispose()\n// on it\nusing var binding = autoValue.Bind();\n\n// you can chain binding callback registration for ease-of-use\nbinding\n  // called whenever the value changes\n  .OnValue(animal =\u003e Console.WriteLine($\"Observing animal {animal}\"))\n  // only called for Dog values\n  .OnValue((Dog dog) =\u003e Console.WriteLine($\"Observing dog {dog.Name}\"))\n  // only called for Cat values\n  .OnValue((Cat cat) =\u003e Console.WriteLine($\"Observing cat {cat.Name}\"));\n\nautoValue.Value = new Dog(\"Brisket\");\n// Observing animal Brisket\n// Observing dog Brisket\nautoValue.Value = new Cat(\"Chibi\");\n// Observing animal Chibi\n// Observing cat Chibi\n```\n\nNote that `AutoValue\u003cT\u003e` allows you to register type-specific callbacks for subtypes of `T` (like `Dog` and `Cat` above). For reference types, this makes for some *very* clean code. Don't use it with value types unless you're okay with them [getting boxed][boxing].\n\n```csharp\nbinding\n  // only observe dog values\n  .OnValue((Dog dog) =\u003e Console.WriteLine($\"Observing dog {dog.Name}\"))\n  // or if you'd rather specify the type as the generic argument instead of as\n  // the lambda argument\n  .OnValue\u003cDog\u003e(dog =\u003e Console.WriteLine($\"Observing dog {dog.Name}\"))\n```\n\nAutoValue also allows you to provide a predicate to further customize which values you're interested in.\n\n```csharp\nbinding.OnValue(\n  (Dog dog) =\u003e Console.WriteLine($\"Observing dog with B name {dog.Name}\"),\n  condition: dog =\u003e dog.Name.StartsWith('B') // customize what you care about\n);\n```\n\n## 🔢 AutoList\n\n`AutoList\u003cT\u003e` is a reactive wrapper around `List\u003cT\u003e`. Bindings will be notified of any changes to the list for as long as they remain subscribed. `AutoList\u003cT\u003e` implements the various `IList\u003cT\u003e` interfaces, so you can generally use it just like a C# list.\n\n```csharp\nvar autoList = new AutoList\u003cAnimal\u003e(\n  [\n    new Cat(\"Pickles\"),\n    new Dog(\"Cookie\"),\n    new Dog(\"Brisket\"),\n    new Cat(\"Sven\")\n  ]\n);\n\nusing var binding = autoList.Bind();\n\nbinding\n  .OnAdd(animal =\u003e Console.WriteLine($\"Animal added: {animal}\"))\n  // or with its index\n  .OnAdd((index, animal) =\u003e\n    Console.WriteLine($\"Animal added at index {index}: {animal}\")\n  )\n  .OnClear(() =\u003e Console.WriteLine(\"List cleared\"))\n  // only called when a Dog is added\n  .OnAdd((Dog dog) =\u003e Console.WriteLine($\"Dog added: {dog.Name}\"))\n  // only called when a Cat is removed\n  .OnRemove((Cat cat) =\u003e Console.WriteLine($\"Cat removed: {cat.Name}\"))\n  .OnUpdate(\n    (previous, current) =\u003e\n      Console.WriteLine($\"Animal updated from {previous} to {current}\")\n  )\n  .OnUpdate(\n    (Dog previous, Dog current) =\u003e\n    Console.WriteLine($\"Dog updated from {previous.Name} to {current.Name}\")\n  )\n  .OnUpdate(\n    (Dog previous, Cat current) =\u003e\n    Console.WriteLine($\"Dog {previous.Name} replaced by Cat {current.Name}\")\n  )\n  // or with its index\n  .OnUpdate((Dog previous, Cat current, int index) =\u003e\n    Console.WriteLine(\n      $\"Dog at index {index} updated from {previous} to Cat {current}\"\n    )\n  )\n  // gets notified of ALL modifications to the list (add, remove, clear, update)\n  .OnModify(() =\u003e Console.WriteLine(\"List changed\"));\n\nautoList.Add(new Dog(\"Chibi\"));\nautoList.RemoveAt(0);\n```\n\nOther method overloads are available for various subtypes, **and each callback can optionally receive the index of the item that was changed**. You can also provide a custom comparer in the constructor.\n\n```csharp\nvar autoListWithComparer = new AutoList\u003cAnimal\u003e([], new MyAnimalComparer());\n```\n\n## 🧦 AutoSet\u003cT\u003e\n\nSometimes, you don't care about tracking a list of things by index. `AutoSet\u003cT\u003e` is a simple reactive wrapper around `HashSet\u003cT\u003e`.\n\n\u003e [!NOTE]\n\u003e Due to memory allocation considerations, `AutoSet\u003cT\u003e` does not implement the full `ISet\u003cT\u003e` interfaces, which would require temporary collections to be created to track the result of batch operations.\n\nBindings will be notified of any changes to the set for as long as they remain subscribed.\n\n```csharp\nvar autoSet = new AutoSet\u003cAnimal\u003e(new HashSet\u003cAnimal\u003e\n{\n  new Cat(\"Pickles\"),\n  new Dog(\"Cookie\"),\n  new Dog(\"Brisket\"),\n  new Cat(\"Sven\")\n});\n\nusing var binding = autoSet.Bind();\n\nbinding\n  .OnAdd(animal =\u003e Console.WriteLine($\"Animal added: {animal}\"))\n  .OnRemove(animal =\u003e Console.WriteLine($\"Animal removed: {animal}\"))\n  // only called when a Dog is added\n  .OnAdd((Dog dog) =\u003e Console.WriteLine($\"Dog added: {dog.Name}\"))\n  // only called when a Cat is removed\n  .OnRemove((Cat cat) =\u003e Console.WriteLine($\"Cat removed: {cat.Name}\"))\n  .OnClear(() =\u003e Console.WriteLine(\"Set cleared\"))\n  // gets notified of ALL modifications to the set (add, remove, clear, update)\n  .OnModify(() =\u003e Console.WriteLine(\"Set changed\"));;\n\nautoSet.Add(new Dog(\"Chibi\"));\nautoSet.Remove(new Cat(\"Pickles\"));\n```\n\n## 🗺️ AutoMap\n\n`AutoMap\u003cTKey, TValue\u003e` is a reactive wrapper around `Dictionary\u003cTKey, TValue\u003e`. Bindings will be notified of any changes to the dictionary for as long as they remain subscribed. `AutoMap\u003cTKey, TValue\u003e` implements the various `IDictionary\u003cTKey, TValue\u003e` interfaces, so you can generally use it just like a C# dictionary.\n\n```csharp\nvar autoMap = new AutoMap\u003cstring, Animal\u003e(new Dictionary\u003cstring, Animal\u003e\n{\n  [\"Pickles\"] = new Cat(\"Pickles\"),\n  [\"Cookie\"] = new Dog(\"Cookie\"),\n  [\"Brisket\"] = new Dog(\"Brisket\"),\n  [\"Sven\"] = new Cat(\"Sven\")\n});\n\nusing var binding = autoMap.Bind();\n\nbinding\n  .OnAdd(\n    (key, animal) =\u003e Console.WriteLine($\"Animal added: {key} -\u003e {animal}\")\n  )\n  .OnRemove((key, animal) =\u003e\n    Console.WriteLine($\"Animal removed: {key} -\u003e {animal}\")\n  )\n  .OnUpdate((key, previous, current) =\u003e\n    Console.WriteLine($\"Animal updated: {key} from {previous} to {current}\")\n  )\n  .OnClear(() =\u003e Console.WriteLine(\"Map cleared\"))\n  .OnModify(() =\u003e Console.WriteLine(\"Map changed\"));\n\nautoMap[\"Chibi\"] = new Dog(\"Chibi\");\nautoMap.Remove(\"Pickles\");\nautoMap[\"Brisket\"] = new Poodle(\"Brisket\");\n```\n\n## 📢 AutoChannel\n\n`AutoChannel` is a broadcast channel for sending messages without storing them. Unlike `AutoCache`, which remembers the last value of each type, `AutoChannel` simply broadcasts value types to all current subscribers and moves on. Think of it as a megaphone at a dog park — you announce something, whoever's listening hears it, but there's no recording of what was said.\n\nSince `AutoChannel` doesn't cache values, it's perfect for event-style notifications where you only care about real-time delivery. We've optimized `AutoChannel` for value types so that it does not box them during broadcast.\n\n\u003e [!NOTE]\n\u003e `AutoChannel` only supports value types (structs). If you need to broadcast reference types, consider wrapping them in a struct or using a different primitive.\n\n\u003e [!TIP]\n\u003e Use `AutoChannel` when you want to broadcast events without storing state. Use `AutoCache` when you need to both broadcast and retrieve the last value of each type, or when you want to suppress duplicate consecutive updates.\n\n```csharp\nreadonly record struct DogBarked(string DogName, int Loudness);\nreadonly record struct CatMeowed(string CatName, bool IsHungry);\nreadonly record struct TreatDispensed(string TreatType);\n\nvar autoChannel = new AutoChannel();\nusing var binding = autoChannel.Bind();\n\nbinding\n  .On\u003cDogBarked\u003e(\n    (bark) =\u003e Console.WriteLine($\"{bark.DogName} barked with loudness {bark.Loudness}\")\n  )\n  .On\u003cCatMeowed\u003e(\n    (meow) =\u003e Console.WriteLine($\"{meow.CatName} meowed. Hungry? {meow.IsHungry}\")\n  )\n  .On\u003cTreatDispensed\u003e(\n    (treat) =\u003e Console.WriteLine($\"Dispensed treat: {treat.TreatType}\")\n  );\n\n// Broadcast events - all subscribers hear them immediately\nautoChannel.Send(new DogBarked(\"Brisket\", 10));\nautoChannel.Send(new CatMeowed(\"Pickles\", true));\nautoChannel.Send(new TreatDispensed(\"Bacon Bits\"));\n```\n\nYou can also use conditions to filter which messages trigger your callbacks:\n\n```csharp\nbinding.On\u003cDogBarked\u003e(\n  (bark) =\u003e Console.WriteLine($\"LOUD DOG ALERT: {bark.DogName}!\"),\n  condition: bark =\u003e bark.Loudness \u003e 7 // only react to loud barks\n);\n```\n\n## 💰 AutoCache\n\n`AutoCache` is a cache which stores values separated by type. On update, it broadcasts to all bindings and stores the\nvalue based on the type given. This can then be retrieved by using the `TryGetValue\u003cT\u003e(out T value)` to get the last value\nupdated of type `T`. Since `AutoCache` doesn't have a generic param, it is especially useful as a message channel that deduplicates\nconsecutive identical updates, or as a lookup-cache for multiple types of data. We've optimized `AutoCache` for value types so that it does not box value types on updates.\nYou might find this pattern familiar if you've used `Chickensoft.LogicBlocks`.\n\n\u003e [!CAUTION]\n\u003e When pushing a value of type `Dog` which derives from `Animal`, `TryGetValue\u003cAnimal\u003e()` will not return the last value\n\u003e updated of type `Dog`. If you desire to get the last `Animal` value updated, you will have to use `Update\u003cAnimal\u003e(new Dog())`\n\u003e instead. Although Binding notifications for `OnUpdate\u003cDog\u003e` or `OnUpdate\u003cAnimal\u003e` will still be called regardless of the type pushed.\n\n\u003e [!NOTE]\n\u003e While `AutoCache` does support reference types, consider using value types instead when initializing new instances on\n\u003e update to avoid allocating unnecessary memory that would need to be immediately collected by the garbage collector.\n\u003e Using value types where possible helps avoid stuttering and hitches by reducing the amount of work that the garbage\n\u003e collector needs to do to clean up reference types on the heap.\n\n```csharp\nreadonly record struct UpdateName(string DogName);\n\nvar autoCache = new AutoCache();\nusing var binding = autoCache.Bind();\n\nbinding\n  .OnUpdate\u003cUpdateName\u003e(\n    (name) =\u003e Console.WriteLine($\"Name Updated: {name}\")\n  )\n\nautoCache.Update(new UpdateName(\"Pickles\"));\nautoCache.Update(new UpdateName(\"Sven\"));\n// After each update, the OnUpdate callback will be called.\n\nif(autoCache.TryGetValue\u003cUpdateName\u003e(out var update))\n{\n  // This would print out \"Last received dog name: Sven\"\n  Console.WriteLine($\"Last received dog name: {update.DogName}\"\n}\n\nbinding\n  .OnUpdate\u003cAnimal\u003e(\n    (animal) =\u003e Console.WriteLine($\"Animal Updated: {animal.Name}\")\n  );\n\n// Store and broadcast a Mouse by its less-specific supertype, Animal\nautoCache.Update\u003cAnimal\u003e(new Mouse(\"Hamtaro\"));\nautoCache.Update(new Dog(\"Cookie\"));\nautoCache.Update(new Cat(\"Pickles\"));\n// OnUpdate\u003cAnimal\u003e will be called 3 times.\n\n//See the caution note above for more information\nautoCache.TryGetValue\u003cAnimal\u003e(out var animal) // animal will be the Mouse - Hamtaro\nautoCache.TryGetValue\u003cDog\u003e(out var dog) // animal will be the Dog - Cookie\nautoCache.TryGetValue\u003cCat\u003e(out var cat) // animal will be the Cat - Pickles\n```\n\n## 🗑️ CompositeDisposable\n`CompositeDisposable` represents a set of disposable resources that are disposed together. It is a utility class for bundling up multiple disposables, so that you can dispose them all with one `Dispose()` call. Sync also provides a `DisposeWith()` extension method for disposables that can be chained for convenience.\n\nInternally `CompositeDisposable` implements a `HashSet\u003cIDisposable\u003e` so that no disposable gets disposed of twice. When calling `Dispose()`, it will dispose and clear out all disposables that have been added, and automatically dispose of any disposable that is added in the future.\n\n\u003e [!TIP]\n\u003e If you want to dispose of all disposables that have been added, but **not** dispose of any newly added disposables, call `Clear()` instead. \n\n```csharp\n// Enemy visualization logic\npublic sealed class EnemyView : IDisposable\n{\n  public Enemy Enemy { get; }\n\n  private CompositeDisposable _disposal = new();\n\n  public EnemyView(Enemy enemy)\n  {\n    Enemy = enemy;\n\n    // listen to changes in the enemy\n    enemy.Health.Bind()\n      .OnValue(...)\n      .DisposeWith(_disposal); // Add binding\n\n    enemy.Attack.Bind()\n      .OnValue(...)\n      .DisposeWith(_disposal); // Add binding\n\n    enemy.Defense.Bind()\n      .OnValue(...)\n      .DisposeWith(_disposal); // Add binding\n  }\n\n  public void Dispose()\n  {\n    _disposal.Dispose(); // Dispose all bindings\n  }\n}\n```\n\n## 🧰 Build Your Own Reactive Primitives\n\nSync primitives are all built on top of a `SyncSubject`. A `SyncSubject` is an object which your own reactive primitive will own and use to notify `SyncBinding`s of changes in your reactive primitive.\n\nYou will have to provide your own `SyncBinding` subclass that's tailored to your reactive primitive. Bespoke bindings for each primitive are what makes Sync's API so pleasant to use, and Sync makes it really easy to create a customized binding.\n\n### Stubbing it Out\n\nLet's build our own implementation of `AutoValue\u003cT\u003e`.\n\nFirst, we'll want a read-only interface for our reactive primitive. All we need to do is inherit from `IAutoObject\u003cTBinding\u003e`, where `TBinding` is the type of binding we'll create for our AutoValue. We can stub that out, too.\n\n```csharp\npublic interface IAutoValue\u003cT\u003e : IAutoObject\u003cAutoValue\u003cT\u003e.Binding\u003e\n{\n  T Value { get; }\n}\n\npublic sealed class AutoValue\u003cT\u003e : IAutoValue\u003cT\u003e\n{\n  public class Binding : SyncBinding {\n    internal Binding(ISyncSubject subject) : base(subject) { }\n  }\n}\n```\n\n\u003e [!TIP]\n\u003e By convention, we nest the binding in the reactive primitive class itself so that it can access private members of the primitive, as well as any of their generic type parameters.\n\n### Atomic Operations\n\nLet's go ahead and implement the required methods for the `IAutoObject` interface. Luckily, we can just forward these to a private `SyncSubject` which handles the deferred event loop system for us. We'll also tell our subject to perform an atomic operation whenever the value is changed, rather than mutating the state right away.\n\n\u003e [!NOTE]\n\u003e Later, we'll implement a method that allows us to know when it's time to actually change the value. This is how `SyncSubject` is able to protect us from [reentrancy] issues.\n\nYou can define an atomic operation by creating a value type struct. It's really easy to use a one-line `readonly record struct` in C# for this, so that's what we'll do.\n\n```csharp\npublic sealed class AutoValue\u003cT\u003e : IAutoValue\u003cT\u003e\n{\n    // Atomic operations\n  private readonly record struct UpdateOp(T Value);\n\n  // ... binding class\n\n  private T _value;\n  private readonly SyncSubject _subject;\n\n  public T Value {\n    get =\u003e _value;\n    set =\u003e _subject.Perform(new UpdateOp(value));\n  }\n\n  public AutoValue(T value) {\n    _value = value;\n    // create a new sync subject that will notify us when it's time to perform\n    // the atomic operations we schedule\n    _subject = new SyncSubject(this);\n  }\n\n  public Binding Bind() =\u003e new Binding(_subject);\n  public void ClearBindings() =\u003e _subject.ClearBindings();\n  public void Dispose() =\u003e _subject.Dispose();\n}\n```\n\n### Performing an Atomic Operation\n\nTo actually perform our `UpdateOp` operation, we'll edit our AutoValue to implement `IPerform\u003cTOp\u003e` for every atomic operation we want to support. Our AutoValue implementation is really simple, so it's just the one atomic operation for now.\n\nWhile we're at it, we'll go ahead and create a *broadcast*. A broadcast is also a value type that is sent to each binding. Atomic operations and broadcasts will often be identical, but not always. It's important to keep them distinct.\n\n```csharp\npublic sealed class AutoValue\u003cT\u003e : IAutoValue\u003cT\u003e,\n    IPerform\u003cAutoValue\u003cT\u003e.UpdateOp\u003e\n{\n  // Atomic operations\n  private readonly record struct UpdateOp(T Value);\n\n  // Broadcasts\n  public readonly record struct UpdateBroadcast(T Value);\n\n  // ... binding class\n\n  // other members\n\n  // Actually perform the atomic operation\n  void IPerform\u003cUpdateOp\u003e.Perform(in UpdateOp op)\n  {\n    if (_value != op.Value) {\n      // only update if it's different\n      return;\n    }\n\n    _value = op.Value;\n\n    // announce change to relevant binding callbacks\n    _subject.Broadcast(new UpdateBroadcast(op.Value));\n  }\n}\n```\n\n### Binding Implementation\n\nNow, the only thing left to do is make our `Binding` class allow the developer to register a callback whenever the value changes.\n\n```csharp\npublic sealed class AutoValue\u003cT\u003e : IAutoValue\u003cT\u003e,\n    IPerform\u003cAutoValue\u003cT\u003e.UpdateOp\u003e,\n    IPerform\u003cAutoValue\u003cT\u003e.SyncOp\u003e\n{\n  // Atomic operations\n  private readonly record struct UpdateOp(T Value);\n  private readonly record struct SyncOp(Action\u003cT\u003e Callback);\n\n  // Broadcasts\n  public readonly record struct UpdateBroadcast(T Value);\n\n  public class Binding : SyncBinding\n  {\n    internal Binding(ISyncSubject subject) : base(subject) { }\n\n    public Binding OnValue(Action\u003cT\u003e callback)\n    {\n      AddCallback((in UpdateBroadcast broadcast) =\u003e callback(broadcast.Value));\n\n      // invoke binding as soon as possible after it's added to give it the\n      // current value immediately. different reactive primitives may or may not\n      // want to do this, depending on their desired behavior.\n      _subject!.Perform(new SyncOp(callback));\n\n      return this; // to let the developer chain callback registration\n    }\n  }\n\n  // ... other members shown above\n\n  // Perform the \"sync\" operation to invoke a callback with the current value\n  // when a binding is first added. This mimics a ReactiveX BehaviorSubject.\n  void IPerform\u003cSyncOp\u003e.Perform(in SyncOp op) =\u003e op.Callback(_value);\n}\n```\n\nNow, anyone can easily create an auto value and bind to it!\n\n```csharp\nvar autoValue = new AutoValue\u003cint\u003e(42);\nusing var binding = autoValue.Bind();\nbinding.OnValue(value =\u003e Console.WriteLine($\"Value changed to {value}\"));\n```\n\n\u003e [!NOTE]\n\u003e The [actual `AutoValue\u003cT\u003e` implementation](./Chickensoft.Sync//src/primitives/AutoValue.cs) has to account for a custom comparer, conditional bindings, and derived types, but it's otherwise almost identical.\n\u003e\n\u003e If you're building your own reactive primitives, take a look at the full source code for `AutoValue\u003cT\u003e`, `AutoList\u003cT\u003e`, `AutoSet\u003cT\u003e`, and `AutoMap\u003cTKey, TValue\u003e` for more examples.\n\n## 🙋‍♀️ Why?\n\nSync is a generalization of the Chickensoft bindings system first seen in [LogicBlocks]. If you've ever used LogicBlocks, you already know how to use Sync!\n\n### 🐣 Simple\n\nExisting .NET reactive programming libraries are stunted by the reigning naming terminologies: either by trying to conform to ReactiveX's loosely defined terminology or .NET's own poorly-named observer APIs. Neither were designed with game development as the primary use case, and both result in poor code readability or correctness for many typical use cases.\n\nAdditionally, [many find Rx.NET just plain confusing and difficult to deal with][rx-confusing].\n\nNot convinced? See how ReactiveX describes its own terminology:\n\n\u003e Each language-specific implementation of ReactiveX has its own naming quirks. There is no canonical naming standard, though there are many commonalities between implementations.\n\u003e\n\u003e Furthermore, some of these names have different implications in other contexts, or seem awkward in the idiom of a particular implementing language.\n\u003e\n\u003e For example there is the onEvent naming pattern (e.g. onNext, onCompleted, onError). In some contexts such names would indicate methods by means of which event handlers are registered. In ReactiveX, however, they name the event handlers themselves. - [ReactiveX Docs](https://reactivex.io/documentation/observable.html)\n\nSince there's \"no canonical naming standard\" and each implementation has \"its own naming quirks\", [*we might as well invent our own simplified terminology*][standards] 🤷‍♀️.\n\n### 🏎️ Performance\n\nSync is pretty performant for what it does. Sync's AutoValue has been benchmarked in comparison to R3's reactive property. You can [see the benchmark source code here](./Chickensoft.Sync.Benchmarks//src/benchmarks/SimpleRepeatedInvoke.cs).\n\nThis is a bit of an apples-to-oranges comparison: Sync primitives like AutoValue protect against reentry and allows reactive subjects to define atomic operations, R3 simply invokes functions immediately every time a value changes. Naturally, R3 is about 8-9 times faster since it has essentially no overhead. Both are very fast and do not allocate memory during the hot path (the results are in nanoseconds — billionths of a second). Both scale linearly with the number of invocations, as you'd expect.\n\nHere's the results on an M1 Max laptop:\n\n| Method           | N    |         Mean |        Error |    StdDev | Alloc |\n|------------------|------|-------------:|-------------:|----------:|------:|\n| ReactiveProperty | 10   |     29.13 ns |     1.002 ns |  0.055 ns |     - |\n| AutoValueSet     | 10   |    255.42 ns |     9.659 ns |  0.529 ns |     - |\n|                  |      |              |              |           |       |\n| ReactiveProperty | 100  |    298.18 ns |    20.567 ns |  1.127 ns |     - |\n| AutoValueSet     | 100  |  2,526.14 ns |   316.602 ns | 17.354 ns |     - |\n|                  |      |              |              |           |       |\n| ReactiveProperty | 1000 |  2,933.28 ns |   337.410 ns | 18.495 ns |     - |\n| AutoValueSet     | 1000 | 24,816.69 ns | 1,512.528 ns | 82.907 ns |     - |\n\nDividing by N to get the average per property set update:\n\n| Method           |     Mean |\n|------------------|---------:|\n| ReactiveProperty |  2.94 ns |\n| AutoValueSet     | 25.21 ns |\n\nWith 1,000,000,000 nanoseconds in a second, that's about **340 million updates per second for R3's `ReactiveProperty`** and **40 million updates per second for Chickensoft.Sync's `AutoValue`**.\n\nOr, for 16 ms frame time in a 60 FPS game, that's about 5.7 million sets per frame for R3 and 666,666 per frame for AutoValue. If you need absolute performance and no guarantees, use R3. If you need deterministic single-threaded execution, use Sync. Both are very fast and do not allocate. For UI work, which typically has latency in terms of microseconds, the choice will not matter at all.\n\n### ✅ Correct By Default\n\nWhen subscribing to changes in a reactive object, your callbacks will observe each change that the object goes through. If you try to mutate the reactive object from those callbacks, you typically want those changes to be deferred until all the callbacks for the current state of the object have finished execution.\n\nBy deferring changes, every callback is executed deterministically and in order for each state that the reactive object passes through. Deferral should still happen synchronously via a loop at the outermost stack level, but reactive programming libraries do not do this by default.\n\nFor example: the .NET Reactive Extensions (Rx.NET) do not protect against reentrancy by default unless you manually *serialize* a reactive subject (not to be confused with the other \"serialization\" for saving and loading). Other libraries for C#, such as the aforementioned [R3] reactive programming library, [do not protect against reentrancy at all](./Chickensoft.Sync.Tests/src/R3Comparison.cs), favoring absolute performance instead. Like all systems, you must evaluate the tradeoffs for your particular use case.\n\n\u003e [!NOTE]\n\u003e Unless you are building absolutely massive systems, picking correctness and ergonomics over absolute performance will most likely increase the chance of success, since it makes refactoring simpler and safer.\n\n---\n\n🐣 Package generated from a 🐤 Chickensoft Template — \u003chttps://chickensoft.games\u003e\n\n[chickensoft-badge]: https://chickensoft.games/img/badges/chickensoft_badge.svg\n[chickensoft-website]: https://chickensoft.games\n[discord-badge]: https://chickensoft.games/img/badges/discord_badge.svg\n[discord]: https://discord.gg/gSjaPgMmYW\n[read-the-docs-badge]: https://chickensoft.games/img/badges/read_the_docs_badge.svg\n[docs]: https://chickensoft.games/docs\n[line-coverage]: Chickensoft.Sync.Tests/badges/line_coverage.svg\n[branch-coverage]: Chickensoft.Sync.Tests/badges/branch_coverage.svg\n[R3]: https://github.com/Cysharp/R3\n[nuget]: https://www.nuget.org/packages/Chickensoft.Sync\n[reentrancy]: https://en.wikipedia.org/wiki/Reentrancy_(computing)\n[boxing]: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing\n[rx-confusing]: https://www.reddit.com/r/dotnet/comments/1ea7lu6/pros_and_cons_of_using_reactive_extensions_rx_in/\n[standards]: https://xkcd.com/927/\n[LogicBlocks]: https://github.com/chickensoft-games/LogicBlocks\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchickensoft-games%2Fsync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchickensoft-games%2Fsync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchickensoft-games%2Fsync/lists"}