Skip to content

Commit

Permalink
feat(renaming): Renamed IdentityMap and EntityEnumWrapper (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
pjechris authored Dec 22, 2023
1 parent c2aeb3c commit f0f1d50
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 203 deletions.
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

0 comments on commit f0f1d50

Please sign in to comment.