From 0d026117d1a701c6de7381192a6df08717ea8181 Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 12:40:36 +0100
Subject: [PATCH 1/7] typos

---
 Sources/Contentful/ArrayResponse.swift | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

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)
             }

From 95b14c9da1d08296c4c30f0bc08c3c2a538fbcb2 Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 12:41:02 +0100
Subject: [PATCH 2/7] typos

---
 Sources/Contentful/Client.swift | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

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)
         }
     }
 }
+

From d9b850f007035ada67c6292f41cd50943c6c0f5e Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 12:41:14 +0100
Subject: [PATCH 3/7] typo

---
 Sources/Contentful/Decodable.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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
                 """)
         }

From 9b0c903ebf178ab5d9c1a5302178a9fd7afff530 Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 12:42:24 +0100
Subject: [PATCH 4/7] improved logic

---
 Sources/Contentful/Entry.swift     | 41 ++++++++++++++++++++++++------
 Sources/Contentful/Link.swift      | 13 +++++++++-
 Sources/Contentful/SyncSpace.swift |  3 +++
 3 files changed, 48 insertions(+), 9 deletions(-)

diff --git a/Sources/Contentful/Entry.swift b/Sources/Contentful/Entry.swift
index 23384e4b..5fd6c024 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.needsResolution
+                            ? link
+                            : link.resolve(against: includedEntries, and: includedAssets)
                     }
+
                     resolvedLocalizableFieldMap[localeCode] = resolvedLinks
+                    SyncSpace.cachedLinks[id + fieldName] = resolvedLinks
+
+                case let oneToOneLink as Link where oneToOneLink.needsResolution == 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,11 @@ 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
+    /// or if the array is empty.
+    var needsResolution: Bool {
+        return isEmpty || contains { $0.needsResolution }
+    }
+}
diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift
index ba5df315..c76e68b8 100644
--- a/Sources/Contentful/Link.swift
+++ b/Sources/Contentful/Link.swift
@@ -107,7 +107,7 @@ public enum Link: Codable {
         return sys
     }
 
-    var isResolved: Bool {
+    var needsResolution: Bool {
         switch self {
         case .asset, .entry, .entryDecodable: return true
         case .unresolved: return false
@@ -176,3 +176,14 @@ public enum Link: Codable {
         case sys
     }
 }
+
+// MARK: - Hashable, Equatable
+extension Link: Hashable, Equatable {
+    public var hashValue: Int {
+        return id.hashValue
+    }
+    
+    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)

From cb668e93460353754b234535343c76ea625d84e3 Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 12:43:59 +0100
Subject: [PATCH 5/7] extracted function to improve readability

---
 Sources/Contentful/Client+Sync.swift | 65 +++++++++++++++-------------
 1 file changed, 36 insertions(+), 29 deletions(-)

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<SyncSpace, Error>) in
+        return fetchDecodable(url: url(endpoint: .sync, parameters: parameters)) { (result: Result<SyncSpace, Error>) 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<SyncSpace>
+    ) {
+        _ = 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))
             }

From efac50e40a2ae265e138befab010980cbf023514 Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 12:52:28 +0100
Subject: [PATCH 6/7] corrected logic

---
 Sources/Contentful/Link.swift | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift
index c76e68b8..61b9a0b3 100644
--- a/Sources/Contentful/Link.swift
+++ b/Sources/Contentful/Link.swift
@@ -109,8 +109,8 @@ public enum Link: Codable {
 
     var needsResolution: Bool {
         switch self {
-        case .asset, .entry, .entryDecodable: return true
-        case .unresolved: return false
+        case .asset, .entry, .entryDecodable: return false
+        case .unresolved: return true
         }
     }
 

From da35f18f224271c1cc16c9f7c8fa9131c626bcba Mon Sep 17 00:00:00 2001
From: Guillermo Moraleda <guillermo.moraleda@external.contentful.com>
Date: Mon, 18 Nov 2024 21:12:39 +0100
Subject: [PATCH 7/7] reverted breaking changes

---
 Sources/Contentful/Entry.swift |  7 +++----
 Sources/Contentful/Link.swift  | 13 +++++++------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/Sources/Contentful/Entry.swift b/Sources/Contentful/Entry.swift
index 5fd6c024..318bf02b 100644
--- a/Sources/Contentful/Entry.swift
+++ b/Sources/Contentful/Entry.swift
@@ -92,7 +92,7 @@ public class Entry: LocalizableResource {
 
                     // Resolve all links
                     let resolvedLinks = oneToManyLinks.map { link -> Link in
-                        link.needsResolution
+                        link.isResolved
                             ? link
                             : link.resolve(against: includedEntries, and: includedAssets)
                     }
@@ -100,7 +100,7 @@ public class Entry: LocalizableResource {
                     resolvedLocalizableFieldMap[localeCode] = resolvedLinks
                     SyncSpace.cachedLinks[id + fieldName] = resolvedLinks
 
-                case let oneToOneLink as Link where oneToOneLink.needsResolution == false:
+                case let oneToOneLink as Link where oneToOneLink.isResolved == false:
                     let resolvedLink = oneToOneLink.resolve(against: includedEntries, and: includedAssets)
                     resolvedLocalizableFieldMap[localeCode] = resolvedLink
 
@@ -129,8 +129,7 @@ extension Entry: ResourceQueryable {
 
 extension [Link] {
     /// Needs resolution if any of the links in the array need resolution
-    /// or if the array is empty.
     var needsResolution: Bool {
-        return isEmpty || contains { $0.needsResolution }
+        return contains { $0.isResolved == false }
     }
 }
diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift
index 61b9a0b3..8c618be1 100644
--- a/Sources/Contentful/Link.swift
+++ b/Sources/Contentful/Link.swift
@@ -107,10 +107,10 @@ public enum Link: Codable {
         return sys
     }
 
-    var needsResolution: Bool {
+    var isResolved: Bool {
         switch self {
-        case .asset, .entry, .entryDecodable: return false
-        case .unresolved: return true
+        case .asset, .entry, .entryDecodable: return true
+        case .unresolved: return false
         }
     }
 
@@ -178,11 +178,12 @@ public enum Link: Codable {
 }
 
 // MARK: - Hashable, Equatable
+
 extension Link: Hashable, Equatable {
-    public var hashValue: Int {
-        return id.hashValue
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
     }
-    
+
     public static func == (lhs: Link, rhs: Link) -> Bool {
         return lhs.id == rhs.id
     }