Refactor PollHistoryService

This commit is contained in:
Alfonso Grillo
2023-01-23 12:04:24 +01:00
parent 9cc9a046fe
commit 9b16774d6a
5 changed files with 130 additions and 137 deletions
@@ -29,7 +29,6 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
case activeEmpty
case pastEmpty
case loading
case loadingWithContent
/// The associated screen
var screenType: Any.Type {
@@ -48,16 +47,19 @@ enum MockPollHistoryScreenState: MockScreenState, CaseIterable {
pollHistoryMode = .past
case .activeEmpty:
pollHistoryMode = .active
pollService.activePollsData = []
pollService.nextPublisher = Empty(completeImmediately: true,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
case .pastEmpty:
pollHistoryMode = .past
pollService.pastPollsData = []
pollService.nextPublisher = Empty(completeImmediately: true,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
case .loading:
pollHistoryMode = .active
pollService.isLoadingPublisher = Just(true).eraseToAnyPublisher()
case .loadingWithContent:
pollHistoryMode = .active
pollService.isLoadingPublisher = [false, true].publisher.eraseToAnyPublisher()
pollService.nextPublisher = Empty(completeImmediately: false,
outputType: TimelinePollDetails.self,
failureType: Error.self).eraseToAnyPublisher()
}
let viewModel = PollHistoryViewModel(mode: pollHistoryMode, pollService: pollService)
@@ -21,9 +21,8 @@ typealias PollHistoryViewModelType = StateStoreViewModel<PollHistoryViewState, P
final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModelProtocol {
private let pollService: PollHistoryServiceProtocol
private var polls: [TimelinePollDetails] = []
private var polls: [TimelinePollDetails]?
private var subcriptions: Set<AnyCancellable> = .init()
private var hasLoadedFirstGroup = false
var completion: ((PollHistoryViewModelResult) -> Void)?
@@ -37,8 +36,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
override func process(viewAction: PollHistoryViewAction) {
switch viewAction {
case .viewAppeared:
setupSubscriptions()
pollService.next()
setupUpdateSubscriptions()
fetchFirstBatch()
case .segmentDidChange:
updateViewState()
}
@@ -46,11 +45,27 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
}
private extension PollHistoryViewModel {
func setupSubscriptions() {
private func fetchFirstBatch() {
state.isLoading = true
pollService
.next()
.collect()
.sink { [weak self] _ in
#warning("Handle errors")
self?.state.isLoading = false
} receiveValue: { [weak self] polls in
self?.polls = polls
self?.updateViewState()
}
.store(in: &subcriptions)
}
func setupUpdateSubscriptions() {
subcriptions.removeAll()
pollService
.pollHistory
.updates
.sink { [weak self] detail in
self?.updatePolls(with: detail)
self?.updateViewState()
@@ -58,53 +73,31 @@ private extension PollHistoryViewModel {
.store(in: &subcriptions)
pollService
.error
.updatesErrors
.sink { detail in
#warning("Handle errors")
}
.store(in: &subcriptions)
let didCompleteFirstFetch = pollService
.isFetching
.filter { $0 == false }
.first()
didCompleteFirstFetch
.sink { isFetching in
self.hasLoadedFirstGroup = true
self.updateViewState()
}
.store(in: &subcriptions)
pollService
.isFetching
.weakAssign(to: \.state.isLoading, on: self)
.store(in: &subcriptions)
}
func updatePolls(with poll: TimelinePollDetails) {
if let matchIndex = polls.firstIndex(where: { $0.id == poll.id }) {
polls[matchIndex] = poll
if let matchIndex = polls?.firstIndex(where: { $0.id == poll.id }) {
polls?[matchIndex] = poll
} else {
polls.append(poll)
polls.sort(by: { $0.startDate > $1.startDate })
polls?.append(poll)
}
}
func updateViewState() {
guard hasLoadedFirstGroup else {
return
}
let renderedPolls: [TimelinePollDetails]
let renderedPolls: [TimelinePollDetails]?
switch context.mode {
case .active:
renderedPolls = polls.filter { $0.closed == false }
renderedPolls = polls?.filter { $0.closed == false }
case .past:
renderedPolls = polls.filter { $0.closed == true }
renderedPolls = polls?.filter { $0.closed == true }
}
state.polls = renderedPolls
state.polls = renderedPolls?.sorted(by: { $0.startDate > $1.startDate })
}
}
@@ -22,25 +22,22 @@ final class PollHistoryService: PollHistoryServiceProtocol {
private let room: MXRoom
private let timeline: MXEventTimeline
private let chunkSizeInDays: UInt
private let pollsSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
private let errorSubject: PassthroughSubject<PollHistoryError, Never> = .init()
private let isFetchingSubject: PassthroughSubject<Bool, Never> = .init()
private var timelineListener: Any?
private let updatesSubject: PassthroughSubject<TimelinePollDetails, Never> = .init()
private let updatesErrorsSubject: PassthroughSubject<Error, Never> = .init()
private var pollAggregators: [String: PollAggregator] = [:]
private var targetTimestamp: Date
private var oldestEventDate: Date = .distantFuture
private var currentBatchSubject: PassthroughSubject<TimelinePollDetails, Error>?
var pollHistory: AnyPublisher<TimelinePollDetails, Never> {
pollsSubject.eraseToAnyPublisher()
var updates: AnyPublisher<TimelinePollDetails, Never> {
updatesSubject.eraseToAnyPublisher()
}
var error: AnyPublisher<PollHistoryError, Never> {
errorSubject.eraseToAnyPublisher()
}
var isFetching: AnyPublisher<Bool, Never> {
isFetchingSubject.eraseToAnyPublisher()
var updatesErrors: AnyPublisher<Error, Never> {
updatesErrorsSubject.eraseToAnyPublisher()
}
init(room: MXRoom, chunkSizeInDays: UInt) {
@@ -51,8 +48,8 @@ final class PollHistoryService: PollHistoryServiceProtocol {
setup(timeline: timeline)
}
func next() {
startPagination()
func next() -> AnyPublisher<TimelinePollDetails, Error> {
currentBatchSubject?.eraseToAnyPublisher() ?? startPagination()
}
}
@@ -64,22 +61,31 @@ private extension PollHistoryService {
func setup(timeline: MXEventTimeline) {
timelineListener = timeline.listenToEvents { [weak self] event, _, _ in
guard let self = self else {
return
}
if event.eventType == .pollStart {
self.aggregatePoll(pollStartEvent: event)
self?.aggregatePoll(pollStartEvent: event)
}
self.oldestEventDate = min(event.originServerDate, self.oldestEventDate)
self?.updateTimestamp(event: event)
}
}
func startPagination() {
isFetchingSubject.send(true)
timeline.resetPagination()
paginate(timeline: timeline)
func updateTimestamp(event: MXEvent) {
oldestEventDate = min(event.originServerDate, oldestEventDate)
}
func startPagination() -> AnyPublisher<TimelinePollDetails, Error> {
let batchSubject = PassthroughSubject<TimelinePollDetails, Error>()
currentBatchSubject = batchSubject
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.timeline.resetPagination()
self.paginate(timeline: self.timeline)
}
return batchSubject.eraseToAnyPublisher()
}
func paginate(timeline: MXEventTimeline) {
@@ -93,15 +99,19 @@ private extension PollHistoryService {
if timeline.canPaginate(.backwards), self.timestampTargetReached == false {
self.paginate(timeline: timeline)
} else {
self.isFetchingSubject.send(false)
self.completeBatch(completion: .finished)
}
case .failure(let error):
self.errorSubject.send(.paginationFailed(error))
self.isFetchingSubject.send(false)
self.completeBatch(completion: .failure(error))
}
}
}
func completeBatch(completion: Subscribers.Completion<Error>) {
currentBatchSubject?.send(completion: completion)
currentBatchSubject = nil
}
func aggregatePoll(pollStartEvent: MXEvent) {
guard pollAggregators[pollStartEvent.eventId] == nil else {
return
@@ -131,14 +141,14 @@ extension PollHistoryService: PollAggregatorDelegate {
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {}
func pollAggregatorDidEndLoading(_ aggregator: PollAggregator) {
pollsSubject.send(.init(poll: aggregator.poll, represent: .started))
currentBatchSubject?.send(.init(poll: aggregator.poll, represent: .started))
}
func pollAggregator(_ aggregator: PollAggregator, didFailWithError: Error) {
errorSubject.send(.pollAggregationFailed(didFailWithError))
updatesErrorsSubject.send(didFailWithError)
}
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
pollsSubject.send(.init(poll: aggregator.poll, represent: .started))
updatesSubject.send(.init(poll: aggregator.poll, represent: .started))
}
}
@@ -17,54 +17,56 @@
import Combine
final class MockPollHistoryService: PollHistoryServiceProtocol {
private let polls: PassthroughSubject<TimelinePollDetails, Never> = .init()
var pollHistory: AnyPublisher<TimelinePollDetails, Never> {
polls.eraseToAnyPublisher()
}
var error: AnyPublisher<PollHistoryError, Never> {
var updates: AnyPublisher<TimelinePollDetails, Never> {
Empty().eraseToAnyPublisher()
}
func next() {
for poll in activePollsData + pastPollsData {
polls.send(poll)
}
}
var isLoadingPublisher: AnyPublisher = Just(false).eraseToAnyPublisher()
var isFetching: AnyPublisher<Bool, Never> {
isLoadingPublisher
}
var activePollsData: [TimelinePollDetails] = (1..<10)
.map { index in
TimelinePollDetails(id: "a\(index)",
question: "Do you like the active poll number \(index)?",
answerOptions: [],
closed: false,
startDate: .init(),
totalAnswerCount: 30,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
var pastPollsData: [TimelinePollDetails] = (1..<10)
.map { index in
TimelinePollDetails(id: "p\(index)",
question: "Do you like the active poll number \(index)?",
answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)],
closed: true,
startDate: .init(),
totalAnswerCount: 30,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
var updatesErrors: AnyPublisher<Error, Never> {
Empty().eraseToAnyPublisher()
}
lazy var nextPublisher: AnyPublisher<TimelinePollDetails, Error> = (activePollsData + pastPollsData)
.publisher
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
func next() -> AnyPublisher<TimelinePollDetails, Error> {
nextPublisher
}
}
private extension MockPollHistoryService {
var activePollsData: [TimelinePollDetails] {
(1..<10)
.map { index in
TimelinePollDetails(id: "a\(index)",
question: "Do you like the active poll number \(index)?",
answerOptions: [],
closed: false,
startDate: .init(),
totalAnswerCount: 30,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
}
var pastPollsData: [TimelinePollDetails] {
(1..<10)
.map { index in
TimelinePollDetails(id: "p\(index)",
question: "Do you like the active poll number \(index)?",
answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)],
closed: true,
startDate: .init(),
totalAnswerCount: 30,
type: .disclosed,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
}
}
@@ -17,22 +17,8 @@
import Combine
protocol PollHistoryServiceProtocol {
/// Publishes poll data as soon they are found in the timeline.
/// Updates are also published here, so clients needs to address duplicates.
var pollHistory: AnyPublisher<TimelinePollDetails, Never> { get }
var updates: AnyPublisher<TimelinePollDetails, Never> { get }
var updatesErrors: AnyPublisher<Error, Never> { get }
/// Publishes whatever errors produced during the sync.
var error: AnyPublisher<PollHistoryError, Never> { get }
/// Ask to fetch the next batch of polls.
/// Concrete implementations can decide what a batch is.
func next()
/// Inform whenever the fetch of a new batch of polls starts or ends.
var isFetching: AnyPublisher<Bool, Never> { get }
}
enum PollHistoryError: Error {
case paginationFailed(Error)
case pollAggregationFailed(Error)
func next() -> AnyPublisher<TimelinePollDetails, Error>
}