diff --git a/Sources/Contentful/ArrayResponse.swift b/Sources/Contentful/ArrayResponse.swift index c2c15f80..75d2e05d 100644 --- a/Sources/Contentful/ArrayResponse.swift +++ b/Sources/Contentful/ArrayResponse.swift @@ -31,7 +31,7 @@ private protocol HomogeneousArray: Array { public struct ArrayResponseError: Decodable { /// The system fields of the error. public struct Sys: Decodable { - /// The identifer of the error. + /// The identifier of the error. public let id: String /// The type identifier for the error. public let type: String @@ -111,7 +111,7 @@ extension HomogeneousArrayResponse: Decodable { if areItemsOfCustomType { // Since self's items are of a custom (i.e. user-defined) type, - // we must accomodate the scenario that included Entries are + // we must accommodate the scenario that included Entries are // heterogeneous, since items can link to whatever custom Entries // the user defined. // As a consequence, we have to use the LinkResolver in order to @@ -182,7 +182,7 @@ extension HomogeneousArrayResponse: Decodable { entriesMap[entry.sys.id] = entry } - // Rememember `Entry`s are classes (passed by reference) so we can change them in place. + // Remember `Entry`s are classes (passed by reference) so we can change them in place. for entry in allIncludedEntries { entry.resolveLinks(against: entriesMap, and: assetsMap) } diff --git a/Sources/Contentful/Client+Sync.swift b/Sources/Contentful/Client+Sync.swift index e13629b9..03b788ab 100755 --- a/Sources/Contentful/Client+Sync.swift +++ b/Sources/Contentful/Client+Sync.swift @@ -17,7 +17,7 @@ extension Client { in order to allow chaining of operations. - Parameters: - - syncSpace: Instance to perform subsqeuent sync on. Empty instance by default. + - syncSpace: Instance to perform subsequent sync on. Empty instance by default. - syncableTypes: The types that can be synchronized. - completion: The completion handler to call when the operation is complete. */ @@ -30,46 +30,53 @@ extension Client { // Preview mode only supports `initialSync` not `nextSync`. The only reason `nextSync` should // be called while in preview mode, is internally by the SDK to finish a multiple page sync. // We are doing a multi page sync only when syncSpace.hasMorePages is true. - if !syncSpace.syncToken.isEmpty, host == Host.preview, syncSpace.hasMorePages == false { + guard !(host == Host.preview && !syncSpace.syncToken.isEmpty && !syncSpace.hasMorePages) else { completion(.failure(SDKError.previewAPIDoesNotSupportSync)) return nil } // Send only sync space parameters when accessing another page. - let parameters: [String: String] - if syncSpace.hasMorePages { - parameters = syncSpace.parameters - } else { - parameters = syncableTypes.parameters + syncSpace.parameters - } + let parameters = syncSpace.hasMorePages + ? syncSpace.parameters + : (syncableTypes.parameters + syncSpace.parameters) - return fetchDecodable( - url: url( - endpoint: .sync, - parameters: parameters - ) - ) { (result: Result) in + return fetchDecodable(url: url(endpoint: .sync, parameters: parameters)) { (result: Result) in switch result { case let .success(newSyncSpace): syncSpace.updateWithDiffs(from: newSyncSpace) - if newSyncSpace.hasMorePages { - self.sync(for: syncSpace, syncableTypes: syncableTypes, then: completion) - } else { - _ = self.fetchContentTypes { contentTypeResult in - switch contentTypeResult { - case let .success(contentTypes): - for entry in syncSpace.entries { - let type = contentTypes.first { $0.sys.id == entry.sys.contentTypeId } - entry.type = type - } - self.persistenceIntegration?.update(with: syncSpace) - completion(.success(syncSpace)) - case let .failure(error): - completion(.failure(error)) - } + // Continue syncing if there are more pages + guard newSyncSpace.hasMorePages else { + self.handleContentTypeFetch(for: syncSpace, completion: completion) + return + } + + // Recursive call to continue syncing + self.sync(for: syncSpace, syncableTypes: syncableTypes, then: completion) + + case let .failure(error): + completion(.failure(error)) + } + } + } + + private func handleContentTypeFetch( + for syncSpace: SyncSpace, + completion: @escaping ResultsHandler + ) { + _ = self.fetchContentTypes { result in + switch result { + case let .success(contentTypes): + for entry in syncSpace.entries { + // Assign content type to each entry in the sync space + entry.type = contentTypes.first { contentType in + contentType.sys.id == entry.sys.contentTypeId } } + + self.persistenceIntegration?.update(with: syncSpace) + completion(.success(syncSpace)) + case let .failure(error): completion(.failure(error)) } diff --git a/Sources/Contentful/Client.swift b/Sources/Contentful/Client.swift index 97e360cb..9a5627ca 100644 --- a/Sources/Contentful/Client.swift +++ b/Sources/Contentful/Client.swift @@ -488,10 +488,11 @@ open class Client { } else { let error = SDKError.unparseableJSON( data: data, - errorMessage: "Unknown error occured during decoding." + errorMessage: "Unknown error occurred during decoding." ) ContentfulLogger.log(.error, message: error.message) return .failure(error) } } } + diff --git a/Sources/Contentful/Decodable.swift b/Sources/Contentful/Decodable.swift index e805272f..f0477297 100644 --- a/Sources/Contentful/Decodable.swift +++ b/Sources/Contentful/Decodable.swift @@ -28,7 +28,7 @@ public extension Decoder { guard let contentTypes = userInfo[.contentTypesContextKey] as? [ContentTypeId: EntryDecodable.Type] else { fatalError( """ - Make sure to pass your content types into the Client intializer + Make sure to pass your content types into the Client initializer so the SDK can properly deserializer your own types if you are using the `fetchMappedEntries` methods """) } diff --git a/Sources/Contentful/Entry.swift b/Sources/Contentful/Entry.swift index 23384e4b..318bf02b 100644 --- a/Sources/Contentful/Entry.swift +++ b/Sources/Contentful/Entry.swift @@ -76,20 +76,37 @@ public class Entry: LocalizableResource { // those that are of type Link. for (localeCode, fieldValueForLocaleCode) in localizableFieldMap { switch fieldValueForLocaleCode { - case let oneToOneLink as Link where oneToOneLink.isResolved == false: - let resolvedLink = oneToOneLink.resolve(against: includedEntries, and: includedAssets) - resolvedLocalizableFieldMap[localeCode] = resolvedLink case let oneToManyLinks as [Link]: + + // If the links do not need resolution, we can skip the resolution + guard oneToManyLinks.needsResolution else { + resolvedLocalizableFieldMap[localeCode] = oneToManyLinks + continue + } + + // Check if the links are already cached + if let resolvedLinks = SyncSpace.cachedLinks[id + fieldName] { + resolvedLocalizableFieldMap[localeCode] = resolvedLinks + continue + } + + // Resolve all links let resolvedLinks = oneToManyLinks.map { link -> Link in - if link.isResolved { - return link - } else { - return link.resolve(against: includedEntries, and: includedAssets) - } + link.isResolved + ? link + : link.resolve(against: includedEntries, and: includedAssets) } + resolvedLocalizableFieldMap[localeCode] = resolvedLinks + SyncSpace.cachedLinks[id + fieldName] = resolvedLinks + + case let oneToOneLink as Link where oneToOneLink.isResolved == false: + let resolvedLink = oneToOneLink.resolve(against: includedEntries, and: includedAssets) + resolvedLocalizableFieldMap[localeCode] = resolvedLink + case let recursiveNode as RecursiveNode: recursiveNode.resolveLinks(against: includedEntries, and: includedAssets) + default: continue } @@ -109,3 +126,10 @@ extension Entry: ResourceQueryable { /// The QueryType for an EntryQuery is Query. public typealias QueryType = Query } + +extension [Link] { + /// Needs resolution if any of the links in the array need resolution + var needsResolution: Bool { + return contains { $0.isResolved == false } + } +} diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift index ba5df315..8c618be1 100644 --- a/Sources/Contentful/Link.swift +++ b/Sources/Contentful/Link.swift @@ -176,3 +176,15 @@ public enum Link: Codable { case sys } } + +// MARK: - Hashable, Equatable + +extension Link: Hashable, Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Link, rhs: Link) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Sources/Contentful/SyncSpace.swift b/Sources/Contentful/SyncSpace.swift index b3590c81..f257dbed 100755 --- a/Sources/Contentful/SyncSpace.swift +++ b/Sources/Contentful/SyncSpace.swift @@ -173,7 +173,10 @@ public final class SyncSpace: Decodable { case items } + static var cachedLinks = [String: [Link]]() + func updateWithDiffs(from syncSpace: SyncSpace) { + Self.cachedLinks = [:] // Resolve all entries in-memory. for entry in entries { entry.resolveLinks(against: entriesMap, and: assetsMap)