Skip to content

Commit

Permalink
Merge branch 'develop' into view-context-cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
laevandus authored Jan 3, 2025
2 parents 12ae0d0 + 0485e99 commit 0bb5ff3
Show file tree
Hide file tree
Showing 25 changed files with 120 additions and 980 deletions.
4 changes: 1 addition & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.7

import Foundation
import PackageDescription
Expand Down Expand Up @@ -56,8 +56,6 @@ let package = Package(
]
)

#if swift(>=5.6)
package.dependencies.append(
.package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.0.0")
)
#endif
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</p>
<p align="center">
<a href="https://getstream.io/chat/docs/sdk/ios/"><img src="https://img.shields.io/badge/iOS-13%2B-lightblue" /></a>
<a href="https://swift.org"><img src="https://img.shields.io/badge/Swift-5.6%2B-orange.svg" /></a>
<a href="https://swift.org"><img src="https://img.shields.io/badge/Swift-5.7%2B-orange.svg" /></a>
<a href="https://github.com/GetStream/stream-chat-swift/actions"><img src="https://github.com/GetStream/stream-chat-swift/actions/workflows/cron-checks.yml/badge.svg" /></a>
<a href="https://sonarcloud.io/summary/new_code?id=GetStream_stream-chat-swift"><img src="https://sonarcloud.io/api/project_badges/measure?project=GetStream_stream-chat-swift&metric=coverage" /></a>
</p>
Expand Down Expand Up @@ -39,7 +39,7 @@ The **StreamChatSwiftUI SDK** is our UI SDK for SwiftUI components. If your appl
- **Familiar behavior**: The UI elements are good platform citizens and behave like native elements; they respect `tintColor`, `layoutMargins`, light/dark mode, dynamic font sizes, etc.
- **Swift native API:** Uses Swift's powerful language features to make the SDK usage easy and type-safe.
- `UIKit` and `SwiftUI` SDKs use native patterns and paradigms from respective UI frameworks: The API follows the design of native system SDKs. It makes integration with your existing code easy and familiar.
- `UIKit` SDK is part of this repository whereas `SwiftUI` SDK is available [here](https://github.com/GetStream/stream-chat-swiftui).
- `UIKit` SDK is part of this repository whereas `SwiftUI` SDK is available [here](https://github.com/GetStream/stream-chat-swiftui).
- **First-class support for `Combine` and `Structured Concurrency`**: Refer to our getting started guides for [Combine](https://getstream.io/chat/docs/sdk/ios/combine/) and [Structured Concurrency](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/).
- **Fully open-source implementation:** You have access to the complete source code of the SDK here on GitHub.
- **Supports iOS 13+:** We proudly support older versions of iOS, so your app can stay available to almost everyone.
Expand Down
5 changes: 0 additions & 5 deletions Sources/StreamChat/Config/StreamRuntimeCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ public enum StreamRuntimeCheck {
/// Enables reusing unchanged converted items in database observers.
public static var _isDatabaseObserverItemReusingEnabled = true

/// For *internal use* only
///
/// Uses version 2 for offline state sync.
public static var _isSyncV2Enabled = true

/// For *internal use* only
///
/// Core Data prefetches data used for creating immutable model objects (faulting is disabled).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,28 +197,6 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt
let channelCount = channelListObserver.items.count
worker.refreshLoadedChannels(for: query, channelCount: channelCount, completion: completion)
}

func resetQuery(
watchedAndSynchedChannelIds: Set<ChannelId>,
synchedChannelIds: Set<ChannelId>,
completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set<ChannelId>), Error>) -> Void
) {
let pageSize = query.pagination.pageSize
worker.resetChannelsQuery(
for: query,
pageSize: pageSize,
watchedAndSynchedChannelIds: watchedAndSynchedChannelIds,
synchedChannelIds: synchedChannelIds
) { [weak self] result in
switch result {
case let .success((newChannels, unwantedCids)):
self?.hasLoadedAllPreviousChannels = newChannels.count < pageSize
completion(.success((newChannels, unwantedCids)))
case let .failure(error):
completion(.failure(error))
}
}
}

// MARK: - Helpers

Expand Down
12 changes: 0 additions & 12 deletions Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,18 +373,6 @@ extension NSManagedObjectContext {
delete(dto)
}

func cleanChannels(cids: Set<ChannelId>) {
let channels = ChannelDTO.load(cids: Array(cids), context: self)
for channelDTO in channels {
channelDTO.resetEphemeralValues()
channelDTO.messages.removeAll()
channelDTO.members.removeAll()
channelDTO.pinnedMessages.removeAll()
channelDTO.reads.removeAll()
channelDTO.oldestMessageAt = nil
}
}

func removeChannels(cids: Set<ChannelId>) {
let channels = ChannelDTO.load(cids: Array(cids), context: self)
channels.forEach(delete)
Expand Down
3 changes: 0 additions & 3 deletions Sources/StreamChat/Database/DatabaseSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,6 @@ protocol ChannelDatabaseSession {
/// Removes channel list query from database.
func delete(query: ChannelListQuery)

/// Cleans a list of channels based on their id
func cleanChannels(cids: Set<ChannelId>)

/// Removes a list of channels based on their id
func removeChannels(cids: Set<ChannelId>)
}
Expand Down
109 changes: 0 additions & 109 deletions Sources/StreamChat/Repositories/SyncOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,6 @@ final class RefreshChannelListOperation: AsyncOperation, @unchecked Sendable {
}
}

final class GetChannelIdsOperation: AsyncOperation, @unchecked Sendable {
init(database: DatabaseContainer, context: SyncContext, activeChannelIds: [ChannelId]) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak database] _, done in
guard let database = database else {
done(.continue)
return
}
database.backgroundReadOnlyContext.perform {
let cids = database.backgroundReadOnlyContext.loadAllChannelListQueries()
.flatMap(\.channels)
.compactMap { try? ChannelId(cid: $0.cid) }
log.info("0. Retrieved channels from existing queries from DB. Count \(cids.count)", subsystems: .offlineSupport)
context.localChannelIds = Set(cids + activeChannelIds)
done(.continue)
}
}
}
}

final class SyncEventsOperation: AsyncOperation, @unchecked Sendable {
init(syncRepository: SyncRepository, context: SyncContext, recovery: Bool) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak syncRepository] _, done in
Expand Down Expand Up @@ -208,96 +189,6 @@ final class WatchChannelOperation: AsyncOperation, @unchecked Sendable {
}
}

final class RefetchChannelListQueryOperation: AsyncOperation, @unchecked Sendable {
init(controller: ChatChannelListController, context: SyncContext) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak controller] _, done in
guard let controller = controller, controller.canBeRecovered else {
done(.continue)
return
}

let query = controller.query

log.info("3 & 4. Refetching channel lists queries & Cleaning up local message history", subsystems: .offlineSupport)
controller.resetQuery(
watchedAndSynchedChannelIds: context.watchedAndSynchedChannelIds,
synchedChannelIds: context.synchedChannelIds
) { result in
Self.handleResult(result, query: query, context: context, done: done)
}
}
}

init(query: ChannelListQuery, channelListUpdater: ChannelListUpdater, context: SyncContext) {
super.init(maxRetries: syncOperationsMaximumRetries) { _, done in
log.info("3 & 4. Refetching channel lists queries (step 2)", subsystems: .offlineSupport)
channelListUpdater.resetChannelsQuery(
for: query,
pageSize: query.pagination.pageSize,
watchedAndSynchedChannelIds: context.watchedAndSynchedChannelIds,
synchedChannelIds: context.synchedChannelIds
) { result in
Self.handleResult(result, query: query, context: context, done: done)
}
}
}

private static func handleResult(
_ result: Result<(synchedAndWatched: [ChatChannel], unwanted: Set<ChannelId>), any Error>,
query: ChannelListQuery,
context: SyncContext,
done: (AsyncOperation.Output) -> Void
) {
switch result {
case let .success((watchedChannels, unwantedCids)):
log.info("Successfully refetched query for \(query.debugDescription)", subsystems: .offlineSupport)
let queryChannelIds = watchedChannels.map(\.cid)
context.watchedAndSynchedChannelIds.formUnion(queryChannelIds)
context.unwantedChannelIds.formUnion(unwantedCids)
done(.continue)
case let .failure(error):
log.error(
"Failed refetching query for \(query.debugDescription): \(error)",
subsystems: .offlineSupport
)
done(.retry)
}
}
}

final class DeleteUnwantedChannelsOperation: AsyncOperation, @unchecked Sendable {
init(database: DatabaseContainer, context: SyncContext) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak database] _, done in
log.info("4. Clean up unwanted channels", subsystems: .offlineSupport)

guard let database = database, !context.unwantedChannelIds.isEmpty else {
done(.continue)
return
}

// We are going to remove those channels that are not present in remote queries, and that have not
// been watched.
database.write { session in
// We remove watchedAndSynched from unwantedChannels because it might happen that a channel marked
// as unwanted in one query, might still be needed in another query (scenario where multiple queries
// are active at the same time).
let idsToRemove = context.unwantedChannelIds.subtracting(context.watchedAndSynchedChannelIds)
session.removeChannels(cids: idsToRemove)
} completion: { error in
if let error = error {
log.error(
"Failed removing unwanted channels: \(error)",
subsystems: .offlineSupport
)
done(.retry)
} else {
done(.continue)
}
}
}
}
}

final class ExecutePendingOfflineActions: AsyncOperation, @unchecked Sendable {
init(offlineRequestsRepository: OfflineRequestsRepository) {
super.init(maxRetries: syncOperationsMaximumRetries) { [weak offlineRequestsRepository] _, done in
Expand Down
101 changes: 3 additions & 98 deletions Sources/StreamChat/Repositories/SyncRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class SyncRepository {
private let database: DatabaseContainer
private let apiClient: APIClient
private let channelListUpdater: ChannelListUpdater
var usesV2Sync = StreamRuntimeCheck._isSyncV2Enabled
let offlineRequestsRepository: OfflineRequestsRepository
let eventNotificationCenter: EventNotificationCenter

Expand Down Expand Up @@ -137,15 +136,11 @@ class SyncRepository {
}
return
}
if self?.usesV2Sync == true {
self?.syncLocalStateV2(lastSyncAt: lastSyncAt, completion: completion)
} else {
self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion)
}
self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion)
}
}

// MARK: - V2
// MARK: -

/// Runs offline tasks and updates the local state for channels
///
Expand All @@ -161,7 +156,7 @@ class SyncRepository {
/// * channel controllers targeting other channels
/// * no channel lists active, but channel controllers are
/// 4. Re-watch channels what we were watching before disconnect
private func syncLocalStateV2(lastSyncAt: Date, completion: @escaping () -> Void) {
private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) {
let context = SyncContext(lastSyncAt: lastSyncAt)
var operations: [Operation] = []
let start = CFAbsoluteTimeGetCurrent()
Expand Down Expand Up @@ -213,96 +208,6 @@ class SyncRepository {
}
operationQueue.addOperations(operations, waitUntilFinished: false)
}

// MARK: - V1

/// Syncs the local state with the server to make sure the local database is up to date.
/// It features queuing, serialization and retries
///
/// [Sync and watch channels](https://www.notion.so/2-Sync-and-watch-channels-ac44feb55de3482f8f0f99e100ca40c6)
/// 1. Call `/sync` endpoint and get missing events for all locally existed channels
/// 2. Start watching open channels
/// 3. Refetch channel lists queries, link only what backend returns (the 1st page)
/// 4. Clean up unwanted channels
/// 5. Run offline actions requests
///
/// - Parameter completion: A block that will get executed upon completion of the synchronization
private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) {
log.info("Starting to recover offline state", subsystems: .offlineSupport)
let context = SyncContext(lastSyncAt: lastSyncAt)
var operations: [Operation] = []

// Enter recovery mode so no other requests are triggered.
apiClient.enterRecoveryMode()

// Run offline actions requests as the first thing
if config.isLocalStorageEnabled {
operations.append(ExecutePendingOfflineActions(offlineRequestsRepository: offlineRequestsRepository))
}

// Get the existing channelIds
let activeChannelIds = activeChannelControllers.allObjects.compactMap(\.cid)
operations.append(GetChannelIdsOperation(database: database, context: context, activeChannelIds: activeChannelIds))

// 1. Call `/sync` endpoint and get missing events for all locally existed channels
operations.append(SyncEventsOperation(syncRepository: self, context: context, recovery: true))

// 2. Start watching open channels.
let watchChannelOperations: [AsyncOperation] = activeChannelControllers.allObjects.map { controller in
WatchChannelOperation(controller: controller, context: context, recovery: true)
}
operations.append(contentsOf: watchChannelOperations)

// 3. Refetch channel lists queries, link only what backend returns (the 1st page)
// We use `context.synchedChannelIds` to keep track of the channels that were synched both in the previous step and
// after each ChannelListController recovery.
let refetchChannelListQueryOperations: [AsyncOperation] = activeChannelListControllers.allObjects
.map { controller in
RefetchChannelListQueryOperation(
controller: controller,
context: context
)
}
operations.append(contentsOf: refetchChannelListQueryOperations)

let channelListQueries: [ChannelListQuery] = {
let queries = activeChannelLists.allObjects
.map(\.query)
.map { ($0.filter.filterHash, $0) }
let uniqueQueries = Dictionary(queries, uniquingKeysWith: { _, last in last })
return Array(uniqueQueries.values)
}()
operations.append(contentsOf: channelListQueries
.map { channelListQuery in
RefetchChannelListQueryOperation(
query: channelListQuery,
channelListUpdater: channelListUpdater,
context: context
)
})

// 4. Clean up unwanted channels
operations.append(DeleteUnwantedChannelsOperation(database: database, context: context))

operations.append(BlockOperation(block: { [weak self] in
log.info("Finished recovering offline state", subsystems: .offlineSupport)
DispatchQueue.main.async {
self?.apiClient.exitRecoveryMode()
completion()
}
}))

// We are making sure the operations happen sequentially one after the other by setting one as the dependency
// of the following one
var previousOperation: Operation?
operations.reversed().forEach { operation in
defer { previousOperation = operation }
guard let previousOperation = previousOperation else { return }
previousOperation.addDependency(operation)
}

operationQueue.addOperations(operations, waitUntilFinished: false)
}

/// Syncs the events for the active chat channels using the last sync date.
/// - Parameter completion: A block that will get executed upon completion of the synchronization
Expand Down
Loading

0 comments on commit 0bb5ff3

Please sign in to comment.