Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(renaming): Renamed IdentityMap and EntityEnumWrapper #63

Merged
merged 3 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Example/Example/Data/MatchRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,38 @@ import Combine
import CohesionKit

class MatchRepository {
private static let identityMap = IdentityMap()
private lazy var identityMap = Self.identityMap
private static let entityStore = EntityStore()
private lazy var entityStore = Self.entityStore

/// load matches with their markets and outcomes from Data.swift
func loadMatches() -> AnyPublisher<[MatchMarkets], Never> {
let matches = MatchMarkets.simulatedMatches

/// store the match and its markets into the identityMap
return identityMap.store(entities: matches, modifiedAt: MatchMarkets.simulatedFetchedDate.stamp).asPublisher
/// store the match and its markets into the entityStore
return entityStore.store(entities: matches, modifiedAt: MatchMarkets.simulatedFetchedDate.stamp).asPublisher
}

/// observe primary (first) match market changes (for this sample changes are generated randomly)
/// - Returns: the match with all its markets including updates for primary market
func observePrimaryMarket(for match: Match) -> AnyPublisher<MatchMarkets, Never> {
let matchMarkets = MatchMarkets.simulatedMatches.first { $0.match.id == match.id }!
var cancellables: Set<AnyCancellable> = []

for outcome in matchMarkets.primaryMarket.outcomes {
generateRandomChanges(for: outcome)
.sink(receiveValue: { [identityMap] in
_ = identityMap.store(entity: $0)
.sink(receiveValue: { [entityStore] in
_ = entityStore.store(entity: $0)
})
.store(in: &cancellables)
}

/// for the test we consider that `loadMatches` was already called and thus markets already stored
return identityMap.find(MatchMarkets.self, id: match.id)!
return entityStore.find(MatchMarkets.self, id: match.id)!
.asPublisher
.handleEvents(receiveCancel: { cancellables.removeAll() })
.eraseToAnyPublisher()
}

private func generateRandomChanges(for outcome: Outcome) -> AnyPublisher<Outcome, Never> {
Timer
.publish(every: 3, on: .main, in: .common)
Expand Down
70 changes: 35 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ Library comes with an [example project](https://github.com/pjechris/CohesionKit/

### Storing an object

First create an instance of `IdentityMap`:
First create an instance of `EntityStore`:

```swift
let identityMap = IdentityMap()
let entityStore = EntityStore()
```

`IdentityMap` let you store `Identifiable` objects:
`EntityStore` let you store `Identifiable` objects:

```swift
struct Book: Identifiable {
Expand All @@ -89,39 +89,39 @@ struct Book: Identifiable {

let book = Book(id: "ABCD", name: "My Book")

identityMap.store(book)
entityStore.store(book)
```

Then You can retrieve the object from anywhere in your code:

```swift
// somewhere else in the code
identityMap.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book")
entityStore.find(Book.self, id: "ABCD") // return Book(id: "ABCD", name: "My Book")
```

### Observing changes

Every time data is updated in `IdentityMap` triggers a notification to any registered observer. To register yourself as an observer just use result from `store` or `find` methods:
Every 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:

```swift
func findBooks() -> some Publisher<[Book], Error> {
// 1. load data using URLSession
URLSession(...)
// 2. store data inside our identityMap
.store(in: identityMap)
// 2. store data inside our entityStore
.store(in: entityStore)
.sink { ... }
.store(in: &cancellables)
}
```

```swift
identityMap.find(Book.self, id: 1)?
entityStore.find(Book.self, id: 1)?
.asPublisher
.sink { ... }
.store(in: &cancellables)
```

> CohesionKit has a [weak memory policy](#weak-memory-management) you should read about. As such, returned value from identityMap.store must be strongly retained to not lose value.
> 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.

> For brievety, next examples will omit `.sink { ... }.store(in:&cancellables)`.

Expand Down Expand Up @@ -160,22 +160,22 @@ let authorBooks = AuthorBooks(
]
)

identityMap.store(authorBooks)
entityStore.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
entityStore.find(Author.self, id: 1) // George R.R Martin
entityStore.find(Book.self, id: "ACK") // A Clash of Kings
entityStore.find(Book.self, id: "ADD") // A Dance with Dragons
```

You can also modify any of them however you want. Notice the change is visible from the object itself AND from aggregate objects:

```swift
let newAuthor = Author(id: 1, name: "George R.R MartinI")

identityMap.store(newAuthor)
entityStore.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]
entityStore.find(Author.self, id: 1) // George R.R MartinI
entityStore.find(AuthorBooks.self, id: 1) // George R.R MartinI + [A Clash of Kings, A Dance with Dragons]
```

> 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.
Expand All @@ -184,7 +184,7 @@ identityMap.find(AuthorBooks.self, id: 1) // George R.R MartinI + [A Clash of Ki

### Storing vs Updating

For now we only focused on `identityMap.store` but CohesionKit comes with another method to store data: `identityMap.update`.
For now we only focused on `entityStore.store` but CohesionKit comes with another method to store data: `entityStore.update`.

Sometimes both can be used but they each have a different purpose:

Expand All @@ -195,10 +195,10 @@ Sometimes both can be used but they each have a different purpose:

### Enum support

Starting with 0.13 library has support for enum types. Note that you'll need to conform to `EntityEnumWrapper` and provide computed getter/setter for each entity you'd like to store.
Starting 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.

```swift
enum MediaType: EntityEnumWrapper {
enum MediaType: EntityWrapper {
case book(Book)
case game(Game)
case tvShow(TvShow)
Expand Down Expand Up @@ -244,21 +244,21 @@ extension AliasKey where T == User {
static let currentUser = AliasKey("user")
}

identityMap.store(currentUser, named: .currentUser)
entityStore.store(currentUser, named: .currentUser)
```

Then request it somewhere else:

```swift
identityMap.find(named: .currentUser) // return the current user
entityStore.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 observes them**. This allow registered observers to be notified when alias value change:

```swift
identityMap.removeAlias(named: .currentUser) // observers will be notified currentUser is nil.
entityStore.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
entityStore.store(newCurrentUser, named: .currentUser) // observers will be notified that currentUser changed even if currentUser was nil before
```

### Stale data
Expand All @@ -268,43 +268,43 @@ When storing data CohesionKit actually require you to set a modification stamp o
By default CohesionKit will use the current date as stamp.

```swift
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
entityStore.store(book) // use default stamp: current date
entityStore.store(book, modifiedAt: Date().stamp) // explicitly use Date time stamp
entityStore.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.

### Weak memory management

CohesionKit has a weak memory policy: objects are kept in `IdentityMap` as long as someone use them.
CohesionKit has a weak memory policy: objects are kept in `EntityStore` as long as someone use them.

To that end you need to retain observers as long as you're interested in the data:

```swift
let book = Book(id: "ACK", title: "A Clash of Kings")
let cancellable = identityMap.store(book) // observer is retained: data is retained
let cancellable = entityStore.store(book) // observer is retained: data is retained

identityMap.find(Book.self, id: "ACK") // return "A Clash of Kings"
entityStore.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.

```swift
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
_ = entityStore.store(book) // observer is not retained and no one else observe this book: data is released

identityMap.find(Book.self, id: "ACK") // return nil
entityStore.find(Book.self, id: "ACK") // return nil
```

```swift
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
var cancellable = entityStore.store(book).asPublisher.sink { ... }
let cancellable2 = entityStore.find(Book.self, id: "ACK") // return a publisher

cancellable = nil

identityMap.find(Book.self, id: "ACK") // return "A Clash of Kings" because cancellable2 still observe this book
entityStore.find(Book.self, id: "ACK") // return "A Clash of Kings" because cancellable2 still observe this book
```

# License
Expand Down
24 changes: 12 additions & 12 deletions Sources/CohesionKit/Combine/Publisher+CohesionKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,34 @@ import Combine
import Foundation

extension Publisher {
/// Stores the `Identifiable` upstream into an identityMap
public func store(in identityMap: IdentityMap, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
/// Stores the `Identifiable` upstream into an entityStore
public func store(in entityStore: EntityStore, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
-> AnyPublisher<Output, Failure> where Output: Identifiable {
map { identityMap.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher }
map { entityStore.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher }
.switchToLatest()
.eraseToAnyPublisher()
}

/// Stores the `Aggregate` upstream into an identityMap
public func store(in identityMap: IdentityMap, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
/// Stores the `Aggregate` upstream into an entityStore
public func store(in entityStore: EntityStore, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
-> AnyPublisher<Output, Failure> where Output: Aggregate {
map { identityMap.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher }
map { entityStore.store(entity: $0, named: named, modifiedAt: modifiedAt).asPublisher }
.switchToLatest()
.eraseToAnyPublisher()
}

/// Stores the upstream collection into an identityMap
public func store(in identityMap: IdentityMap, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
/// Stores the upstream collection into an entityStore
public func store(in entityStore: EntityStore, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
-> AnyPublisher<[Output.Element], Failure> where Output: Collection, Output.Element: Identifiable {
map { identityMap.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher }
map { entityStore.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher }
.switchToLatest()
.eraseToAnyPublisher()
}

/// Stores the upstream collection into an identityMap
public func store(in identityMap: IdentityMap, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
/// Stores the upstream collection into an entityStore
public func store(in entityStore: EntityStore, named: AliasKey<Output>? = nil, modifiedAt: Stamp = Date().stamp)
-> AnyPublisher<[Output.Element], Failure> where Output: Collection, Output.Element: Aggregate {
map { identityMap.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher }
map { entityStore.store(entities: $0, named: named, modifiedAt: modifiedAt).asPublisher }
.switchToLatest()
.eraseToAnyPublisher()
}
Expand Down
21 changes: 0 additions & 21 deletions Sources/CohesionKit/Entity/EntityEnumWrapper.swift

This file was deleted.

24 changes: 24 additions & 0 deletions Sources/CohesionKit/Entity/EntityWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@available(unavailable, renamed: "EntityWrapper")
public typealias EntityEnumWrapper = EntityWrapper

/// A type wrapping one or more Identifiable types.
/// You should rarely need to use this type. However it can happens to have a non Aggregate object containing Identifiable
/// objects to group them (for consistency or naming). This is especially true with enum cases.
public protocol EntityWrapper {
/// Entities contained by all cases relative to the parent container
/// - Returns: entities contained in the wrapper
////
/// Example:
//// ```swift
/// enum MyEnum: EntityWrapper {
/// case a(A)
/// case b(B)
///
/// // note: you would also need to create computed getter/setter for a and b
/// func wrappedEntitiesKeyPaths<Root>(relativeTo root: WritableKeyPath<Root, Self>) -> [PartialIdentifiableKeyPath<Root>] {
/// [.init(root.appending(\.a)), .init(root.appending(\.b))]
/// }
/// }
/// ```
func wrappedEntitiesKeyPaths<Root>(relativeTo parent: WritableKeyPath<Root, Self>) -> [PartialIdentifiableKeyPath<Root>]
}
13 changes: 8 additions & 5 deletions Sources/CohesionKit/EntityStore.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Foundation

@available(*, deprecated, renamed: "EntityStore")
public typealias IdentityMap = EntityStore

/// Manages entities lifecycle and synchronisation
public class IdentityMap {
public class EntityStore {
public typealias Update<T> = (inout T) -> Void

/// the queue on which identity map do its heavy work
Expand All @@ -11,9 +14,9 @@ public class IdentityMap {

private(set) var storage: EntitiesStorage = EntitiesStorage()
private(set) var refAliases: AliasStorage = [:]
private lazy var storeVisitor = IdentityMapStoreVisitor(identityMap: self)
private lazy var storeVisitor = EntityStoreStoreVisitor(entityStore: self)

/// Create a new IdentityMap instance optionally with a queue and a logger
/// Create a new EntityStore instance optionally with a queue and a logger
/// - Parameter queue: the queue on which to receive updates. If nil identitymap will create its own.
/// - Parameter logger: a logger to follow/debug identity internal state
public convenience init(queue: DispatchQueue? = nil, logger: Logger? = nil) {
Expand Down Expand Up @@ -217,7 +220,7 @@ public class IdentityMap {

// MARK: Update

extension IdentityMap {
extension EntityStore {
/// Updates an **already stored** entity using a closure. Useful to update a few properties or when you assume the entity
/// should already be stored.
/// Note: the closure is evaluated before checking `modifiedAt`. As such the closure execution does not mean
Expand Down Expand Up @@ -341,7 +344,7 @@ extension IdentityMap {

// MARK: Delete

extension IdentityMap {
extension EntityStore {
/// Removes an alias from the storage
public func removeAlias<T>(named: AliasKey<T>) {
refAliases[named] = nil
Expand Down
Loading
Loading