{"id":15391236,"url":"https://github.com/pjechris/cohesionkit","last_synced_at":"2025-12-11T22:57:57.697Z","repository":{"id":37039957,"uuid":"365269905","full_name":"pjechris/CohesionKit","owner":"pjechris","description":"Single source of truth library","archived":false,"fork":false,"pushed_at":"2025-03-14T23:19:09.000Z","size":1404,"stargazers_count":7,"open_issues_count":6,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-15T22:49:15.749Z","etag":null,"topics":["combine-framework","coredataless","data-stream","domain-driven-design","identifiable","identity-map","identity-mapper","ios","live-data","live-updates","reactive-programming","relationship","single-source-of-truth","source-of-truth","storage","swift","swift-combine","swift-package-manager","synchronization","type-safe"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pjechris.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":["pjechris"]}},"created_at":"2021-05-07T15:03:17.000Z","updated_at":"2025-03-14T23:18:11.000Z","dependencies_parsed_at":"2023-12-20T08:33:30.491Z","dependency_job_id":"77fa458f-c5f6-4249-b587-4f3a48875158","html_url":"https://github.com/pjechris/CohesionKit","commit_stats":{"total_commits":85,"total_committers":1,"mean_commits":85.0,"dds":0.0,"last_synced_commit":"2c0ca282d21b723cda3a878d3cd133b2cd1ccad5"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjechris%2FCohesionKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjechris%2FCohesionKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjechris%2FCohesionKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjechris%2FCohesionKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pjechris","download_url":"https://codeload.github.com/pjechris/CohesionKit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249167434,"owners_count":21223505,"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","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":["combine-framework","coredataless","data-stream","domain-driven-design","identifiable","identity-map","identity-mapper","ios","live-data","live-updates","reactive-programming","relationship","single-source-of-truth","source-of-truth","storage","swift","swift-combine","swift-package-manager","synchronization","type-safe"],"created_at":"2024-10-01T15:10:26.029Z","updated_at":"2025-12-11T22:57:57.631Z","avatar_url":"https://github.com/pjechris.png","language":"Swift","readme":"\n# CohesionKit - Single Source of Truth\n\n\u003cp\u003e\n  \u003cimg alt=\"swift\" src=\"https://img.shields.io/badge/Swift-5.6%2B-orange?logo=swift\u0026logoColor=white\"/\u003e\n  \u003cimg alt=\"platforms\" src=\"https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS-lightgrey\" /\u003e\n  \u003cimg alt=\"test\" src=\"https://github.com/pjechris/CohesionKit/actions/workflows/test.yml/badge.svg\" /\u003e\n  \u003ca href=\"https://twitter.com/pjechris\"\u003e\n    \u003cimg alt=\"twitter\" src=\"https://img.shields.io/badge/follow-pjechris-1DA1F2?logo=twitter\u0026logoColor=white\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nKeep your models synchronized in your app and never have any inconsistency anymore. Designed using latest Swift features.\n\n## Why using CohesionKit?\n\n- 🔁 You need realtime synchronisation (websockets)\n- 🌐 You have multiple data sources (REST, CoreData, websocket, phone Contacts, Google Maps, etc...)\n- 🪶 You look for a full Swift lightweight tool\n- 🗃️ You want to use structs\n\n### Features\n\n- [x] 🦺 Thread safe\n- [x] 🪶 Lighweight (\u003c 600 lines of code)\n- [x] 🪪 Working with plain Swift `struct` and `Identifiable` objects\n- [x] 🔀 Support for Combine\n- [x] 🧠 In-memory storage\n- [x] 🐾 Low memory footprint\n- [x] 🐪 Strongly typed\n\n### Where to put CohesionKit in my stack?\n\nCohesionKit being a Single Source of Truth solution it handles your objects lifecycle and synchronization from *any* source.\n\nYou should put CohesionKit in front of your data sources (REST API, GraphQL, ...) before returning data to your app.\n\n```mermaid\nsequenceDiagram\n    autonumber\n\n\t\tYourApp -\u003e\u003eDataSource: findBooks\n\t\tDataSource -\u003e\u003eGraphQL: query findBooks\n\t\tGraphQL --\u003e\u003eDataSource: FindBooksQueryResult\n\t\tDataSource -\u003e\u003eCohesionKit: store books [A,B,C]\n\t\tCohesionKit --\u003e\u003e YourApp: Publisher\u003c[A,B,C]\u003e\n\n\t\tWebSocket -\u003e\u003e WebSocketListener: book A updated\n\t\tWebSocketListener -\u003e\u003e CohesionKit: update book A\n\t\tCohesionKit --\u003e\u003e YourApp: Publisher\u003c[A,B,C]\u003e\n```\n\n\n## Installation\n\n- Swift Package Manager\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/pjechris/CohesionKit.git\", .upToNextMajor(from: \"0.7.0\"))\n]\n```\n\n## Examples\n\nLibrary comes with an [example project](https://github.com/pjechris/CohesionKit/tree/main/Example) so you can see a real case usage. It mostly shows:\n\n- How to store data in the library\n- How to retrieve and update that data for realtime\n- How data is synchronised throughout multiple screens\n\n## Getting started\n\n### Storing an object\n\nFirst create an instance of `EntityStore`:\n\n```swift\nlet entityStore = EntityStore()\n```\n\n`EntityStore` let you store `Identifiable` objects:\n\n```swift\nstruct Book: Identifiable {\n  let id: String\n  let title: String\n}\n\nlet book = Book(id: \"ABCD\", name: \"My Book\")\n\nentityStore.store(book)\n```\n\nThen You can retrieve the object from anywhere in your code:\n\n```swift\n// somewhere else in the code\nentityStore.find(Book.self, id: \"ABCD\") // return Book(id: \"ABCD\", name: \"My Book\")\n```\n\n### Observing changes\n\nEvery time data is updated in `EntityStore` triggers a notification to any registered observer. To register yourself as an observer just use result from `store` or `find` methods:\n\n```swift\nfunc findBooks() -\u003e some Publisher\u003c[Book], Error\u003e {\n  // 1. load data using URLSession\n  URLSession(...)\n  // 2. store data inside our entityStore\n    .store(in: entityStore)\n    .sink { ... }\n    .store(in: \u0026cancellables)\n}\n```\n\n```swift\nentityStore.find(Book.self, id: 1)?\n  .asPublisher\n  .sink { ... }\n  .store(in: \u0026cancellables)\n```\n\n\u003e CohesionKit has a [weak memory policy](#weak-memory-management) you should read about. As such, returned value from entityStore.store must be strongly retained to not lose value.\n\n\u003e For brievety, next examples will omit `.sink { ... }.store(in:\u0026cancellables)`.\n\n### Relational objects\n\nTo store objects containing nested identity objects you need to make them conform to one protocol: `Aggregate`.\n\n```swift\nstruct AuthorBooks: Aggregate {\n  var id: Author.ID { author.id }\n\n  var author: Author\n  var books: [Book]\n\n  // `nestedEntitiesKeyPaths` must list all Identifiable/Aggregate this object contain\n  var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath\u003cSelf\u003e] {\n    [.init(\\.author), .init(\\.books)]\n  }\n}\n```\n\nCohesionKit then handles synchronisation for the three entities:\n\n- AuthorBook\n- Author\n- Book\n\n\u003e Only writable keypath are accepted. Using a KeyPath (let) will result in error: \"Key path value type KeyPath\u003cXX\u003e cannot be converted to contextual type WritableKeyPath\u003cXX\u003e\"\n\nThis gives you the ability to retrieve them independently from each other:\n\n```swift\nlet authorBooks = AuthorBooks(\n    author: Author(id: 1, name: \"George R.R Martin\"),\n    books: [\n      Book(id: \"ACK\", title: \"A Clash of Kings\"),\n      Book(id: \"ADD\", title: \"A Dance with Dragons\")\n    ]\n)\n\nentityStore.store(authorBooks)\n\nentityStore.find(Author.self, id: 1) // George R.R Martin\nentityStore.find(Book.self, id: \"ACK\") // A Clash of Kings\nentityStore.find(Book.self, id: \"ADD\") // A Dance with Dragons\n```\n\nYou can also modify any of them however you want. Notice the change is visible from the object itself AND from aggregate objects:\n\n```swift\nlet newAuthor = Author(id: 1, name: \"George R.R MartinI\")\n\nentityStore.store(newAuthor)\n\nentityStore.find(Author.self, id: 1) // George R.R MartinI\nentityStore.find(AuthorBooks.self, id: 1) // George R.R MartinI + [A Clash of Kings, A Dance with Dragons]\n```\n\n\u003e You might think about storing books on `Author` directly (`author.books`). In this case `Author` needs to implement `Aggregate` and declare `books` as nested entity.\n\u003e\n\u003e However I strongly advise you to not nest `Identifiable` objects into other `Identifiable` objects. Read [Handling relationships](https://swiftunwrap.com/article/modeling-done-right/) article if you want to know more about this subject.\n\n### Storing vs Updating\n\nFor now we only focused on `entityStore.store` but CohesionKit comes with another method to store data: `entityStore.update`.\n\nSometimes both can be used but they each have a different purpose:\n\n1. `store` is suited for storing full data retrieved from webservices, like `GET /user` for instance\n2. `update` is usually used for partial data. It's also the preferred method when receiving events from websockets.\n\n## Advanced topics\n\n### Enum support\n\nStarting with 0.13 library has support for enum types. Note that you'll need to conform to `EntityWrapper` and provide computed getter/setter for each entity you'd like to store.\n\n```swift\nenum MediaType: EntityWrapper {\n  case book(Book)\n  case game(Game)\n  case tvShow(TvShow)\n\n  func wrappedEntitiesKeyPaths\u003cRoot\u003e(relativeTo parent: WritableKeyPath\u003cRoot, Self\u003e) -\u003e [PartialIdentifiableKeyPath\u003cRoot\u003e] {\n    [.init(parent.appending(\\.book)), .init(parent.appending(\\.game)), .init(parent.appending(\\.tvShow))]\n  }\n\n  var book: Book? {\n    get { ... }\n    set { ... }\n  }\n\n  var game: Game? {\n    get { ... }\n    set { ... }\n  }\n\n  var tvShow: TvShow? {\n    get { ... }\n    set { ... }\n  }\n}\n\nstruct AuthorMedia: Aggregate {\n  var author: Author\n  var media: MediaType\n\n  var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath\u003cSelf\u003e] {\n    [.init(\\.author), .init(wrapper: \\.media)]\n  }\n}\n```\n\n### Aliases\n\nSometimes you need to retrieve data without knowing the object id. Common case is current user.\n\nCohesionKit provides a suitable mechanism: aliases. Aliases allow you to register and find entities using a key.\n\n```swift\nextension AliasKey where T == User {\n  static let currentUser = AliasKey(\"user\")\n}\n\nentityStore.store(currentUser, named: .currentUser)\n```\n\nThen request it somewhere else:\n\n```swift\nentityStore.find(named: .currentUser) // return the current user\n```\n\nCompared to regular entities, aliased objects are long-live objects: they will be kept in the storage **even if no one observes them**. This allow registered observers to be notified when alias value change:\n\n```swift\nentityStore.removeAlias(named: .currentUser) // observers will be notified currentUser is nil.\n\nentityStore.store(newCurrentUser, named: .currentUser) // observers will be notified that currentUser changed even if currentUser was nil before\n```\n\n### Stale data\n\nWhen storing data CohesionKit actually require you to set a modification stamp on it. `Stamp` is used as a marker to compare data freshness: the higher stamp is the more recent data is.\n\nBy default CohesionKit will use the current date as stamp.\n\n```swift\nentityStore.store(book) // use default stamp: current date\nentityStore.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp\nentityStore.store(book, modifiedAt: 9000) // any Double value is valid\n```\n\nIf for some reason you try to store data with a stamp lower than the already stamped stored data then the update will be discarded.\n\n### Weak memory management\n\nCohesionKit has a weak memory policy: objects are kept in `EntityStore` as long as someone use them.\n\nTo that end you need to retain observers as long as you're interested in the data:\n\n```swift\nlet book = Book(id: \"ACK\", title: \"A Clash of Kings\")\nlet cancellable = entityStore.store(book) // observer is retained: data is retained\n\nentityStore.find(Book.self, id: \"ACK\") // return  \"A Clash of Kings\"\n```\n\nIf you don't create/retain observers then once entities have no more observers they will be automatically discarded from the storage.\n\n```swift\nlet book = Book(id: \"ACK\", title: \"A Clash of Kings\")\n_ = entityStore.store(book) // observer is not retained and no one else observe this book: data is released\n\nentityStore.find(Book.self, id: \"ACK\") // return nil\n```\n\n```swift\nlet book = Book(id: \"ACK\", title: \"A Clash of Kings\")\nvar cancellable = entityStore.store(book).asPublisher.sink { ... }\nlet cancellable2 = entityStore.find(Book.self, id: \"ACK\") // return a publisher\n\ncancellable = nil\n\nentityStore.find(Book.self, id: \"ACK\") // return \"A Clash of Kings\" because cancellable2 still observe this book\n```\n\n# License\n\nThis project is released under the MIT License. Please see the LICENSE file for details.\n","funding_links":["https://github.com/sponsors/pjechris"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpjechris%2Fcohesionkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpjechris%2Fcohesionkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpjechris%2Fcohesionkit/lists"}