Keep your models synchronized in your app and never have any inconsistency anymore. Designed using latest Swift features.
struct
and Identifiable
objectsCohesionKit being a Single Source of Truth solution it handles your objects lifecycle and synchronization from any source.
You should put CohesionKit in front of your data sources (REST API, GraphQL, ...) before returning data to your app.
sequenceDiagram
autonumber
YourApp ->>DataSource: findBooks
DataSource ->>GraphQL: query findBooks
GraphQL -->>DataSource: FindBooksQueryResult
DataSource ->>CohesionKit: store books [A,B,C]
CohesionKit -->> YourApp: Publisher<[A,B,C]>
WebSocket ->> WebSocketListener: book A updated
WebSocketListener ->> CohesionKit: store book A
CohesionKit -->> YourApp: Publisher<[A,B,C]>
dependencies: [
.package(url: "https://github.com/pjechris/CohesionKit.git", .upToNextMajor(from: "0.7.0"))
]
Library comes with an example project so you can see a real case usage. It mostly shows:
First create an instance of IdentityMap
:
let identityMap = IdentityMap()
IdentityMap
let you store Identifiable
objects:
struct Book: Identifiable {
let id: String
let title: String
}
let book = Book(id: "ABCD", name: "My Book")
identityMap.store(book)
Your can then retrieve the object anywhere in your code:
// somewhere else in the code
identityMap.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book")
Every time data is updated in IdentityMap
will trigger a notification to any registered observer. To register yourself as an observer just use result from store
or find
methods:
func findBooks() {
// 1. load data using URLSession
URLSession(...)
// 2. store data inside our identityMap
.store(in: identityMap)
.sink {ย ... }
.store(in: &cancellables)
}
identityMap.find(Book.self, id: 1)?
.asPublisher
.sinkย {ย ... }
.store(in: &cancellables)
CohesionKit has a weak memory policy you should understand.
To store objects containing other objects you need to make them conform to one protocol: Aggregate
.
struct AuthorBooks: Aggregate
var id: Author.ID {ย author.id }
let author: Author
let books: [Book]
// `nestedEntitiesKeyPaths` must list all Identifiable/Aggregate this object contain
var nestedEntitiesKeyPaths: [PartialIdentifiableKeyPath<Self>] {
[.init(\.author), .init(\.books)]
}
}
CohesionKit will then handle synchronisation for the three entities:
This allow you to retrieve them independently from each other:
let authorBooks = AuthorBooks(
author: Author(id: 1, name: "George R.R Martin"),
books: [
Book(id: "ACK", title: "A Clash of Kings"),
Book(id: "ADD", title: "A Dance with Dragons")
]
)
identityMap.store(authorBooks)
identityMap.find(Author.self, id: 1) // George R.R Martin
identityMap.find(Book.self, id: "ACK") // A Clash of Kings
identityMap.find(Book.self, id: "ADD") // A Dance with Dragons
You can also modify any of them however you want:
let newAuthor = Author(id: 1, name: "George R.R MartinI")
identityMap.store(newAuthor)
identityMap.find(Author.self, id: 1) // George R.R MartinI
identityMap.find(AuthorBooks.self, id: 1 // George R.R MartinI + [A Clash of Kings, A Dance with Dragons]
Sometimes you need to retrieve data without knowing the id. Common scenario is current user.
CohesionKit provide a suitable mechanism: aliases. Aliases allow you to register and find entities using a key.
extension AliasKey where T == User {
static let currentUser = AliasKey("user")
}
identityMap.store(currentUser, named: \.currentUser)
Then request it somewhere else:
identityMap.find(named: \.currentUser) // return the current user
Compared to regular entities aliased objects are long-live objects: they will be kept in the storage even if no one observe them. This allow registered observers to be notified when alias value change:
identityMap.removeAlias(named: \.currentUser) // observers will be notified currentUser is nil.
identityMap.store(newCurrentUser, named: \.currentUser) // observers will be notified that currentUser changed even if currentUser was nil before
When 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.
By default CohesionKit will use the current date as stamp.
identityMap.store(book) // use default stamp: current date
identityMap.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp
identityMap.store(book, modifiedAt: 9000) // any Double value is valid
If for some reason you try to store data with a stamp lower than the already stamped stored data then the update will be discarded.
CohesionKit has a weak memory policy: objects are kept in IdentityMap
as long as someone use them.
To that end you need to retain observers as long as you're interested in the data:
let book = Book(id: "ACK", title: "A Clash of Kings")
let cancellable = identityMap.store(book) // observer is retained: data is retained
identityMap.find(Book.self, id: "ACK") // return "A Clash of Kings"
If you don't create/retain observers then once entities have no more observers they will be automatically discarded from the storage.
let book = Book(id: "ACK", title: "A Clash of Kings")
_ = identityMap.store(book) // observer is not retained and no one else observe this book: data is released
identityMap.find(Book.self, id: "ACK") // return nil
let book = let book = Book(id: "ACK", title: "A Clash of Kings")
var cancellable = identityMap.store(book).asPublisher.sink {ย ... }
let cancellable2 = identityMap.find(Book.self, id: "ACK") // return a publisher
cancellable = nil
identityMap.find(Book.self, id: "ADD") // return "A Clash of Kings" because cancellable2 still observe this book
This project is released under the MIT License. Please see the LICENSE file for details.
link |
Stars: 4 |
Last commit: 2 weeks ago |
Full Changelog: https://github.com/pjechris/CohesionKit/compare/0.8.0...0.9.0
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics