From afe5203df9adb68d78fd42a2f54660647f878d8b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 20 Dec 2022 21:58:06 +0100 Subject: [PATCH 01/55] LocalStore --- Sources/ParseSwift/Objects/ParseObject.swift | 9 + Sources/ParseSwift/Parse.swift | 2 + Sources/ParseSwift/ParseConstants.swift | 1 + Sources/ParseSwift/Storage/LocalStorage.swift | 170 ++++++++++++++++++ .../ParseSwift/Storage/ParseFileManager.swift | 27 +++ .../ParseSwift/Types/ParseConfiguration.swift | 36 ++++ Sources/ParseSwift/Types/Query.swift | 37 +++- Sources/ParseSwift/Types/QueryWhere.swift | 10 ++ 8 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 Sources/ParseSwift/Storage/LocalStorage.swift diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 84db2f227..0ca0fbe60 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1209,10 +1209,12 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, callbackQueue: callbackQueue) + completion(.success(object)) } catch { let defaultError = ParseError(code: .unknownError, @@ -1224,6 +1226,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, @@ -1249,6 +1252,7 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1263,6 +1267,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1287,6 +1292,7 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1301,6 +1307,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1325,6 +1332,7 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { + try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1339,6 +1347,7 @@ extension ParseObject { } } #else + try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index cdf961312..e96e596b7 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -17,6 +17,7 @@ internal func initialize(applicationId: String, masterKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + offlinePolicy: ParseConfiguration.OfflinePolicy = .disabled, requiringCustomObjectIds: Bool = false, usingTransactions: Bool = false, usingEqualQueryConstraint: Bool = false, @@ -39,6 +40,7 @@ internal func initialize(applicationId: String, masterKey: masterKey, serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, + offlinePolicy: offlinePolicy, requiringCustomObjectIds: requiringCustomObjectIds, usingTransactions: usingTransactions, usingEqualQueryConstraint: usingEqualQueryConstraint, diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 8f54a5856..009e22d59 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -15,6 +15,7 @@ enum ParseConstants { static let fileManagementPrivateDocumentsDirectory = "Private Documents/" static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" + static let fileObjectsDirectory = "Objects" static let bundlePrefix = "com.parse.ParseSwift" static let batchLimit = 50 static let includeAllKey = "*" diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift new file mode 100644 index 000000000..56a23ed74 --- /dev/null +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -0,0 +1,170 @@ +// +// LocalStorage.swift +// +// +// Created by Damian Van de Kauter on 03/12/2022. +// + +import Foundation + +internal struct LocalStorage { + + static func save(_ object: T, + queryIdentifier: String?) throws { + let fileManager = FileManager.default + let objectData = try ParseCoding.jsonEncoder().encode(object) + + guard let objectId = object.objectId else { + throw ParseError(code: .unknownError, message: "Object has no valid objectId") + } + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + + if fileManager.fileExists(atPath: objectPath.path) { + try objectData.write(to: objectPath) + } else { + fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) + } + + if let queryIdentifier = queryIdentifier { + try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) + } + } + + static func save(_ objects: [T], + queryIdentifier: String?) throws { + let fileManager = FileManager.default + + var successObjects: [T] = [] + for object in objects { + let objectData = try ParseCoding.jsonEncoder().encode(object) + guard let objectId = object.objectId else { + throw ParseError(code: .unknownError, message: "Object has no valid objectId") + } + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + + if fileManager.fileExists(atPath: objectPath.path) { + try objectData.write(to: objectPath) + } else { + fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) + } + + successObjects.append(object) + } + + if let queryIdentifier = queryIdentifier { + try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) + } + } + + static func get(_ type: U.Type, + queryIdentifier: String) throws -> [U]? { + guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } + + var allObjects: [U] = [] + for queryObject in queryObjects { + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) + + let objectData = try Data(contentsOf: objectPath) + if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { + allObjects.append(object) + } + } + + return (allObjects.isEmpty ? nil : allObjects) + } + + static func saveQueryObjects(_ objects: [T], + queryIdentifier: String) throws { + var queryObjects = try getQueryObjects() + queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) + } + + static func getQueryObjects() throws -> [String : [QueryObject]] { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + + if fileManager.fileExists(atPath: queryObjectsPath.path) { + let jsonData = try Data(contentsOf: queryObjectsPath) + let queryObjects = try? ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + return queryObjects ?? [:] + } else { + return [:] + } + } +} + +internal struct QueryObject: Codable { + let objectId: String + let className: String + + init(_ object : T) throws { + guard let objectId = object.objectId else { + throw ParseError(code: .unknownError, message: "Object has no valid objectId") + } + self.objectId = objectId + self.className = object.className + } +} + +internal extension ParseObject { + + func saveLocally(method: Method, + queryIdentifier: String? = nil) throws { + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + if Parse.configuration.isRequiringCustomObjectIds { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } else { + throw ParseError(code: .unknownError, message: "Enable custom objectIds") + } + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + } + } +} + +internal extension Sequence where Element: ParseObject { + + func saveLocally(method: Method, + queryIdentifier: String? = nil) throws { + let objects = map { $0 } + + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + } + } +} diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index bde16c2ee..81fc3609b 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -227,6 +227,33 @@ public extension ParseFileManager { .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) } + + /** + The default directory for all `ParseObject`'s. + - returns: The objects directory. + - throws: An error of type `ParseError`. + */ + static func objectsDirectory(className: String? = nil) throws -> URL { + guard let fileManager = ParseFileManager(), + let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else { + throw ParseError(code: .unknownError, message: "Cannot create ParseFileManager") + } + let objectsDirectory = defaultDirectoryPath + .appendingPathComponent(ParseConstants.fileObjectsDirectory, + isDirectory: true) + try fileManager.createDirectoryIfNeeded(objectsDirectory.path) + + if let className = className { + let classDirectory = objectsDirectory + .appendingPathComponent(className, + isDirectory: true) + try fileManager.createDirectoryIfNeeded(classDirectory.path) + + return classDirectory + } else { + return objectsDirectory + } + } /** Check if a file exists in the Swift SDK download directory. diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index 1fdf8f92c..cba96f339 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -39,6 +39,9 @@ public struct ParseConfiguration { /// The live query server URL to connect to Parse Server. public internal(set) var liveQuerysServerURL: URL? + + /// Determines wheter or not objects need to be saved locally. + public internal(set) var offlinePolicy: OfflinePolicy /// Requires `objectId`'s to be created on the client. public internal(set) var isRequiringCustomObjectIds = false @@ -123,6 +126,7 @@ public struct ParseConfiguration { specified when using the SDK on a server. - parameter serverURL: The server URL to connect to Parse Server. - parameter liveQueryServerURL: The live query server URL to connect to Parse Server. + - parameter OfflinePolicy: When enabled, objects will be stored locally for offline usage. - parameter requiringCustomObjectIds: Requires `objectId`'s to be created on the client side for each object. Must be enabled on the server to work. - parameter usingTransactions: Use transactions when saving/updating multiple objects. @@ -166,6 +170,7 @@ public struct ParseConfiguration { webhookKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + offlinePolicy: OfflinePolicy = .disabled, requiringCustomObjectIds: Bool = false, usingTransactions: Bool = false, usingEqualQueryConstraint: Bool = false, @@ -187,6 +192,7 @@ public struct ParseConfiguration { self.masterKey = masterKey self.serverURL = serverURL self.liveQuerysServerURL = liveQueryServerURL + self.offlinePolicy = offlinePolicy self.isRequiringCustomObjectIds = requiringCustomObjectIds self.isUsingTransactions = usingTransactions self.isUsingEqualQueryConstraint = usingEqualQueryConstraint @@ -389,4 +395,34 @@ public struct ParseConfiguration { authentication: authentication) self.isMigratingFromObjcSDK = migratingFromObjcSDK } + + public enum OfflinePolicy { + + /** + When using the `create` Policy, you can get, create and save objects when offline. + - warning: Using this Policy requires you to enable `allowingCustomObjectIds`. + */ + case create + + /** + When using the `save` Policy, you can get and save objects when offline. + */ + case save + + /** + When using the `disabled` Policy, offline usage is disabled. + */ + case disabled + } +} + +extension ParseConfiguration.OfflinePolicy { + + var canCreate: Bool { + return self == .create + } + + var enabled: Bool { + return self == .create || self == .save + } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index acb1e6c79..21c57d7ed 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -20,6 +20,7 @@ public struct Query: ParseTypeable where T: ParseObject { internal var keys: Set? internal var include: Set? internal var order: [Order]? + internal var useLocalStore: Bool = false internal var isCount: Bool? internal var explain: Bool? internal var hint: AnyCodable? @@ -498,7 +499,23 @@ extension Query: Queryable { if limit == 0 { return [ResultType]() } - return try findCommand().execute(options: options) + if useLocalStore { + do { + let objects = try findCommand().execute(options: options) + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + return objects + } catch let parseError { + if parseError.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { + return localObjects + } else { + throw parseError + } + } + } else { + return try findCommand().execute(options: options) + } } /** @@ -548,7 +565,23 @@ extension Query: Queryable { do { try findCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if useLocalStore { + switch result { + case .success(let objects): + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { + completion(.success(localObjects)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Types/QueryWhere.swift b/Sources/ParseSwift/Types/QueryWhere.swift index 54d15a202..ee0d24878 100644 --- a/Sources/ParseSwift/Types/QueryWhere.swift +++ b/Sources/ParseSwift/Types/QueryWhere.swift @@ -32,6 +32,16 @@ public struct QueryWhere: ParseTypeable { } } } + + internal var queryIdentifier: String { + guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), + let descriptionString = String(data: jsonData, encoding: .utf8) else { + return "" + } + return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) + } } public extension QueryWhere { From 36920d410a26a2c6ed52da6254f5321eee5e7d79 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 20 Dec 2022 22:00:11 +0100 Subject: [PATCH 02/55] FindExplain not working (yet) --- Sources/ParseSwift/Types/Query.swift | 67 +++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 21c57d7ed..c00b0ab95 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -538,10 +538,33 @@ extension Query: Queryable { if limit == 0 { return [U]() } - if !usingMongoDB { - return try findExplainCommand().execute(options: options) + if useLocalStore { + do { + if !usingMongoDB { + let objects = try findExplainCommand().execute(options: options) + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + return objects + } else { + let objects = try findExplainMongoCommand().execute(options: options) + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + return objects + } + } catch let parseError { + if parseError.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(U.self, queryIdentifier: self.where.queryIdentifier) { + return localObjects + } else { + throw parseError + } + } } else { - return try findExplainMongoCommand().execute(options: options) + if !usingMongoDB { + return try findExplainCommand().execute(options: options) + } else { + return try findExplainMongoCommand().execute(options: options) + } } } @@ -621,7 +644,24 @@ extension Query: Queryable { do { try findExplainCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if useLocalStore { + switch result { + case .success(let objects): + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.connectionFailed), + let localObjects = LocalStorage.get([U].self, + queryIdentifier: self.where.queryIdentifier) { + completion(.success(localObjects)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, @@ -634,7 +674,24 @@ extension Query: Queryable { do { try findExplainMongoCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - completion(result) + if useLocalStore { + switch result { + case .success(let objects): + try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.connectionFailed), + let localObjects = LocalStorage.get([U].self, + queryIdentifier: self.where.queryIdentifier) { + completion(.success(localObjects)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, From 22fb1346b0bf8258810786d618094da784810cfe Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 20 Dec 2022 22:02:41 +0100 Subject: [PATCH 03/55] Revert "FindExplain not working (yet)" This reverts commit 36920d410a26a2c6ed52da6254f5321eee5e7d79. --- Sources/ParseSwift/Types/Query.swift | 67 +++------------------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index c00b0ab95..21c57d7ed 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -538,33 +538,10 @@ extension Query: Queryable { if limit == 0 { return [U]() } - if useLocalStore { - do { - if !usingMongoDB { - let objects = try findExplainCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - return objects - } else { - let objects = try findExplainMongoCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - return objects - } - } catch let parseError { - if parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(U.self, queryIdentifier: self.where.queryIdentifier) { - return localObjects - } else { - throw parseError - } - } + if !usingMongoDB { + return try findExplainCommand().execute(options: options) } else { - if !usingMongoDB { - return try findExplainCommand().execute(options: options) - } else { - return try findExplainMongoCommand().execute(options: options) - } + return try findExplainMongoCommand().execute(options: options) } } @@ -644,24 +621,7 @@ extension Query: Queryable { do { try findExplainCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - if useLocalStore { - switch result { - case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - completion(result) - case .failure(let failure): - if failure.equalsTo(.connectionFailed), - let localObjects = LocalStorage.get([U].self, - queryIdentifier: self.where.queryIdentifier) { - completion(.success(localObjects)) - } else { - completion(.failure(failure)) - } - } - } else { - completion(result) - } + completion(result) } } catch { let parseError = ParseError(code: .unknownError, @@ -674,24 +634,7 @@ extension Query: Queryable { do { try findExplainMongoCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - if useLocalStore { - switch result { - case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) - - completion(result) - case .failure(let failure): - if failure.equalsTo(.connectionFailed), - let localObjects = LocalStorage.get([U].self, - queryIdentifier: self.where.queryIdentifier) { - completion(.success(localObjects)) - } else { - completion(.failure(failure)) - } - } - } else { - completion(result) - } + completion(result) } } catch { let parseError = ParseError(code: .unknownError, From 40f21ab0b13087128a3ec5d9cce42c775fa0a3f3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:24:18 +0100 Subject: [PATCH 04/55] Fix --- Sources/ParseSwift/Parse.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index e96e596b7..555361798 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -228,6 +228,7 @@ public func initialize( masterKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + offlinePolicy: ParseConfiguration.OfflinePolicy = .disabled, requiringCustomObjectIds: Bool = false, usingTransactions: Bool = false, usingEqualQueryConstraint: Bool = false, @@ -250,6 +251,7 @@ public func initialize( masterKey: masterKey, serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, + offlinePolicy: offlinePolicy, requiringCustomObjectIds: requiringCustomObjectIds, usingTransactions: usingTransactions, usingEqualQueryConstraint: usingEqualQueryConstraint, From ad7e586a7db7d2911de790faea919d8d03c0ad93 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:38:42 +0100 Subject: [PATCH 05/55] Fix, Print --- Sources/ParseSwift/Storage/LocalStorage.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 56a23ed74..6de62e203 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -11,6 +11,7 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { + print("[LocalStorage] save object") let fileManager = FileManager.default let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -20,6 +21,7 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -34,6 +36,7 @@ internal struct LocalStorage { static func save(_ objects: [T], queryIdentifier: String?) throws { + print("[LocalStorage] save objects") let fileManager = FileManager.default var successObjects: [T] = [] @@ -45,6 +48,7 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) + print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -80,8 +84,21 @@ internal struct LocalStorage { static func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) + + let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) + + if fileManager.fileExists(atPath: queryObjectsPath.path) { + try jsonData.write(to: queryObjectsPath) + } else { + fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil) + } } static func getQueryObjects() throws -> [String : [QueryObject]] { @@ -92,8 +109,7 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) - let queryObjects = try? ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) - return queryObjects ?? [:] + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) } else { return [:] } From ccb65b3480b29c8158c857eff1814ba7579a7ccc Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:44:11 +0100 Subject: [PATCH 06/55] Fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 6de62e203..1ece0c971 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -29,6 +29,7 @@ internal struct LocalStorage { fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) } + print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) } @@ -59,6 +60,7 @@ internal struct LocalStorage { successObjects.append(object) } + print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) } @@ -66,6 +68,8 @@ internal struct LocalStorage { static func get(_ type: U.Type, queryIdentifier: String) throws -> [U]? { + print("[LocalStorage] get objects") + print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] From 81fccd6dcaf34e23ac6ef60e77dafffbc6a05ed3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 19:54:06 +0100 Subject: [PATCH 07/55] useLocalStore fix --- Sources/ParseSwift/Types/Query.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 21c57d7ed..0b643f2d9 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -436,6 +436,17 @@ public struct Query: ParseTypeable where T: ParseObject { mutableQuery.order = keys return mutableQuery } + + /** + Sort the results of the query based on the `Order` enum. + - parameter keys: An array of keys to order by. + - returns: The mutated instance of query for easy chaining. + */ + public func useLocalStore(_ state: Bool = true) -> Query { + var mutableQuery = self + mutableQuery.useLocalStore = state + return mutableQuery + } /** A variadic list of selected fields to receive updates on when the `Query` is used as a From fffd3580e7573cae6408c7634430410be99a4866 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:02:27 +0100 Subject: [PATCH 08/55] Filename change --- Sources/ParseSwift/ParseConstants.swift | 1 + Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 009e22d59..a86679262 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -16,6 +16,7 @@ enum ParseConstants { static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" static let fileObjectsDirectory = "Objects" + static let queryObjectsFile = "QueryObjects" static let bundlePrefix = "com.parse.ParseSwift" static let batchLimit = 50 static let includeAllKey = "*" diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 1ece0c971..01e8d81ae 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -91,7 +91,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) @@ -109,7 +109,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent("QueryObjects.json") + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) From 30a309b89fd71fbdec5c60b9bc57c7165b7b570d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:10:46 +0100 Subject: [PATCH 09/55] Test --- Sources/ParseSwift/Types/Query.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 0b643f2d9..df24dbf9a 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -583,12 +583,12 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { - completion(.success(localObjects)) - } else { + //if failure.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) //{ + completion(.success(localObjects ?? [])) + /*} else { completion(.failure(failure)) - } + }*/ } } else { completion(result) From cd32e3084e4ce28c3ccded3329a2a31f6a05ea07 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:16:39 +0100 Subject: [PATCH 10/55] Changed queryIdentifier --- Sources/ParseSwift/Types/Query.swift | 10 ++++++++++ Sources/ParseSwift/Types/QueryWhere.swift | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index df24dbf9a..f6c5255cd 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -45,6 +45,16 @@ public struct Query: ParseTypeable where T: ParseObject { public var className: String { Self.className } + + internal var queryIdentifier: String { + guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), + let descriptionString = String(data: jsonData, encoding: .utf8) else { + return className + } + return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) + } struct AggregateBody: Codable where T: ParseObject { let pipeline: [[String: AnyCodable]]? diff --git a/Sources/ParseSwift/Types/QueryWhere.swift b/Sources/ParseSwift/Types/QueryWhere.swift index ee0d24878..54d15a202 100644 --- a/Sources/ParseSwift/Types/QueryWhere.swift +++ b/Sources/ParseSwift/Types/QueryWhere.swift @@ -32,16 +32,6 @@ public struct QueryWhere: ParseTypeable { } } } - - internal var queryIdentifier: String { - guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), - let descriptionString = String(data: jsonData, encoding: .utf8) else { - return "" - } - return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) - } } public extension QueryWhere { From 9b0cea9383170a3e4b0f9066608da1a7f2a5cedc Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:18:14 +0100 Subject: [PATCH 11/55] Fix --- Sources/ParseSwift/Types/Query.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index f6c5255cd..0138cb5b3 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -523,12 +523,12 @@ extension Query: Queryable { if useLocalStore { do { let objects = try findCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) return objects } catch let parseError { if parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) { + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { throw parseError @@ -589,12 +589,12 @@ extension Query: Queryable { if useLocalStore { switch result { case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: self.where.queryIdentifier) + try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) completion(result) case .failure(let failure): //if failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: self.where.queryIdentifier) //{ + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) //{ completion(.success(localObjects ?? [])) /*} else { completion(.failure(failure)) From c455849f236cdae1acfd7a13471c4e54de4f563c Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:20:53 +0100 Subject: [PATCH 12/55] Classname --- Sources/ParseSwift/Types/Query.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 0138cb5b3..e676dcbe6 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -51,9 +51,9 @@ public struct Query: ParseTypeable where T: ParseObject { let descriptionString = String(data: jsonData, encoding: .utf8) else { return className } - return descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) + return className + descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) } struct AggregateBody: Codable where T: ParseObject { From 6157b30391940e8e9e8d71dc9950a1999f8270f7 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:45:38 +0100 Subject: [PATCH 13/55] QueryDate, print --- Sources/ParseSwift/Storage/LocalStorage.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 01e8d81ae..1c0f479b3 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -74,11 +74,15 @@ internal struct LocalStorage { var allObjects: [U] = [] for queryObject in queryObjects { + print("[LocalStorage] \(queryObject.objectId)") let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) + print("[LocalStorage] \(objectPath)") let objectData = try Data(contentsOf: objectPath) + print("[LocalStorage] objectData: \(objectData)") if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { + print("[LocalStorage] object: \(object)") allObjects.append(object) } } @@ -123,6 +127,7 @@ internal struct LocalStorage { internal struct QueryObject: Codable { let objectId: String let className: String + let queryDate: Date init(_ object : T) throws { guard let objectId = object.objectId else { @@ -130,6 +135,7 @@ internal struct QueryObject: Codable { } self.objectId = objectId self.className = object.className + self.queryDate = Date() } } From 35e1c5fefe17bb956594b3a2a77e79bb60d34ea5 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 20:53:59 +0100 Subject: [PATCH 14/55] print --- Sources/ParseSwift/Storage/LocalStorage.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 1c0f479b3..85aa37500 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -71,6 +71,7 @@ internal struct LocalStorage { print("[LocalStorage] get objects") print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } + print("[LocalStorage] queryObjects: \(queryObjects)") var allObjects: [U] = [] for queryObject in queryObjects { @@ -114,9 +115,11 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) + print("[LocalStorage] queryObjectsPath: \(queryObjectsPath)") if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) + print("[LocalStorage] jsonData: \(jsonData)") return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) } else { return [:] From cab5ae7a37e6be2387b48cae9b37d4eeaabef691 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:00:58 +0100 Subject: [PATCH 15/55] Print decode error --- Sources/ParseSwift/Storage/LocalStorage.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 85aa37500..5b4108371 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -120,7 +120,12 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) print("[LocalStorage] jsonData: \(jsonData)") - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + do { + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + } catch { + print("[LocalStorage] error: \(error)") + throw error + } } else { return [:] } From a5e575045cb9083007dfc6b6611c1202f8770476 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:05:54 +0100 Subject: [PATCH 16/55] queryIdentifiers --- Sources/ParseSwift/Storage/LocalStorage.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 5b4108371..e031bdbdf 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -70,7 +70,9 @@ internal struct LocalStorage { queryIdentifier: String) throws -> [U]? { print("[LocalStorage] get objects") print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") - guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } + let queryIdentifiers = try getQueryObjects() + print("[LocalStorage] queryIdentifiers: \(queryIdentifiers)") + guard let queryObjects = queryIdentifiers[queryIdentifier] else { return nil } print("[LocalStorage] queryObjects: \(queryObjects)") var allObjects: [U] = [] @@ -115,17 +117,10 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) - print("[LocalStorage] queryObjectsPath: \(queryObjectsPath)") if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) - print("[LocalStorage] jsonData: \(jsonData)") - do { - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) - } catch { - print("[LocalStorage] error: \(error)") - throw error - } + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) } else { return [:] } From e9e8237810c6e7d7049469338274ce686df85625 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:54:20 +0100 Subject: [PATCH 17/55] Sets fix --- Sources/ParseSwift/Types/Query.swift | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index e676dcbe6..cefdf1adf 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -47,13 +47,29 @@ public struct Query: ParseTypeable where T: ParseObject { } internal var queryIdentifier: String { - guard let jsonData = try? ParseCoding.jsonEncoder().encode(self), + var mutableQuery = self + mutableQuery.keys = nil + mutableQuery.include = nil + mutableQuery.excludeKeys = nil + + guard let jsonData = try? ParseCoding.jsonEncoder().encode(mutableQuery), let descriptionString = String(data: jsonData, encoding: .utf8) else { return className } - return className + descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) + + let sortedSets = ( + (keys?.sorted(by: { $0 < $1 }) ?? []) + + (include?.sorted(by: { $0 < $1 }) ?? []) + + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + ).joined(separator: "") //Sets need to be sorted to maintain the same queryIdentifier + + return ( + className + + sortedSets + + descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) + ) } struct AggregateBody: Codable where T: ParseObject { From 7d3f12c3f7b09a9fa383ce7fab6ac6babce504ce Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 21:56:43 +0100 Subject: [PATCH 18/55] Fix --- Sources/ParseSwift/Types/Query.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index cefdf1adf..e17456250 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -66,10 +66,10 @@ public struct Query: ParseTypeable where T: ParseObject { return ( className + sortedSets + - descriptionString.replacingOccurrences(of: "[^A-Za-z0-9]+", - with: "", - options: [.regularExpression]) - ) + descriptionString + ).replacingOccurrences(of: "[^A-Za-z0-9]+", + with: "", + options: [.regularExpression]) } struct AggregateBody: Codable where T: ParseObject { From ecfeec853ddf8d6b1102ba500036468debfe3928 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:01:56 +0100 Subject: [PATCH 19/55] Cleaned --- Sources/ParseSwift/Storage/LocalStorage.swift | 16 ++-------------- Sources/ParseSwift/Types/Query.swift | 10 +++++----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index e031bdbdf..d8582b707 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -11,7 +11,6 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { - print("[LocalStorage] save object") let fileManager = FileManager.default let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -21,7 +20,6 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) - print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -29,7 +27,6 @@ internal struct LocalStorage { fileManager.createFile(atPath: objectPath.path, contents: objectData, attributes: nil) } - print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects([object], queryIdentifier: queryIdentifier) } @@ -37,7 +34,6 @@ internal struct LocalStorage { static func save(_ objects: [T], queryIdentifier: String?) throws { - print("[LocalStorage] save objects") let fileManager = FileManager.default var successObjects: [T] = [] @@ -49,7 +45,6 @@ internal struct LocalStorage { let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) let objectPath = objectsDirectoryPath.appendingPathComponent(objectId) - print("[LocalStorage] objectPath: \(objectPath)") if fileManager.fileExists(atPath: objectPath.path) { try objectData.write(to: objectPath) @@ -60,7 +55,6 @@ internal struct LocalStorage { successObjects.append(object) } - print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") if let queryIdentifier = queryIdentifier { try self.saveQueryObjects(successObjects, queryIdentifier: queryIdentifier) } @@ -70,22 +64,16 @@ internal struct LocalStorage { queryIdentifier: String) throws -> [U]? { print("[LocalStorage] get objects") print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") - let queryIdentifiers = try getQueryObjects() - print("[LocalStorage] queryIdentifiers: \(queryIdentifiers)") - guard let queryObjects = queryIdentifiers[queryIdentifier] else { return nil } - print("[LocalStorage] queryObjects: \(queryObjects)") + guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] for queryObject in queryObjects { - print("[LocalStorage] \(queryObject.objectId)") + print("[LocalStorage] get id: \(queryObject.objectId)") let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) - print("[LocalStorage] \(objectPath)") let objectData = try Data(contentsOf: objectPath) - print("[LocalStorage] objectData: \(objectData)") if let object = try? ParseCoding.jsonDecoder().decode(U.self, from: objectData) { - print("[LocalStorage] object: \(object)") allObjects.append(object) } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index e17456250..d064c711c 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -609,12 +609,12 @@ extension Query: Queryable { completion(result) case .failure(let failure): - //if failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) //{ - completion(.success(localObjects ?? [])) - /*} else { + if failure.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObjects)) + } else { completion(.failure(failure)) - }*/ + } } } else { completion(result) From 0a375fbe5cebe7432e3e4655b375b3af8dcafd99 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:25:35 +0100 Subject: [PATCH 20/55] sorted Sets names --- Sources/ParseSwift/Types/Query.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index d064c711c..4d90b66c6 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -57,10 +57,14 @@ public struct Query: ParseTypeable where T: ParseObject { return className } + let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) + let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) + let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + let sortedSets = ( - (keys?.sorted(by: { $0 < $1 }) ?? []) + - (include?.sorted(by: { $0 < $1 }) ?? []) + - (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + sortedKeys + + sortedInclude + + sortedExcludeKeys ).joined(separator: "") //Sets need to be sorted to maintain the same queryIdentifier return ( From 3b2469c13933d3b2bf9513f6d9a7034ad27e3add Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:26:12 +0100 Subject: [PATCH 21/55] Comment --- Sources/ParseSwift/Types/Query.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 4d90b66c6..23a28f74b 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -57,6 +57,7 @@ public struct Query: ParseTypeable where T: ParseObject { return className } + //Sets need to be sorted to maintain the same queryIdentifier let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) @@ -65,7 +66,7 @@ public struct Query: ParseTypeable where T: ParseObject { sortedKeys + sortedInclude + sortedExcludeKeys - ).joined(separator: "") //Sets need to be sorted to maintain the same queryIdentifier + ).joined(separator: "") return ( className + From 1746e2974bf4aa75c457b08e821c6190512a5a87 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:27:34 +0100 Subject: [PATCH 22/55] Fix: added fields set --- Sources/ParseSwift/Types/Query.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 23a28f74b..dca903d78 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -51,6 +51,7 @@ public struct Query: ParseTypeable where T: ParseObject { mutableQuery.keys = nil mutableQuery.include = nil mutableQuery.excludeKeys = nil + mutableQuery.fields = nil guard let jsonData = try? ParseCoding.jsonEncoder().encode(mutableQuery), let descriptionString = String(data: jsonData, encoding: .utf8) else { @@ -61,11 +62,13 @@ public struct Query: ParseTypeable where T: ParseObject { let sortedKeys = (keys?.count == 0 ? [] : ["keys"]) + (keys?.sorted(by: { $0 < $1 }) ?? []) let sortedInclude = (include?.count == 0 ? [] : ["include"]) + (include?.sorted(by: { $0 < $1 }) ?? []) let sortedExcludeKeys = (excludeKeys?.count == 0 ? [] : ["excludeKeys"]) + (excludeKeys?.sorted(by: { $0 < $1 }) ?? []) + let sortedFieldsKeys = (fields?.count == 0 ? [] : ["fields"]) + (fields?.sorted(by: { $0 < $1 }) ?? []) let sortedSets = ( sortedKeys + sortedInclude + - sortedExcludeKeys + sortedExcludeKeys + + sortedFieldsKeys ).joined(separator: "") return ( From 2d6a9da21be355c6926bd75447c971592f6cb2a6 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:32:04 +0100 Subject: [PATCH 23/55] Print responseError --- Sources/ParseSwift/Extensions/URLSession.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 60ff25e92..e18eb75f1 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -176,6 +176,8 @@ internal extension URLSession { ) { dataTask(with: request) { (responseData, urlResponse, responseError) in + print("responseError") + print(responseError) guard let httpResponse = urlResponse as? HTTPURLResponse else { completion(self.makeResult(request: request, responseData: responseData, From faba3289535d370c68318c461aa17048c25129a9 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:35:07 +0100 Subject: [PATCH 24/55] Removed print --- Sources/ParseSwift/Extensions/URLSession.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index e18eb75f1..60ff25e92 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -176,8 +176,6 @@ internal extension URLSession { ) { dataTask(with: request) { (responseData, urlResponse, responseError) in - print("responseError") - print(responseError) guard let httpResponse = urlResponse as? HTTPURLResponse else { completion(self.makeResult(request: request, responseData: responseData, From aa645984f801c1346212f559dee8d7c77c5fc8ba Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:40:13 +0100 Subject: [PATCH 25/55] Created no internet connection error --- Sources/ParseSwift/Extensions/URLSession.swift | 9 +++++++-- Sources/ParseSwift/Types/ParseError.swift | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 60ff25e92..53f7e959e 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -66,11 +66,16 @@ internal extension URLSession { responseError: Error?, mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { - guard let parseError = responseError as? ParseError else { + if responseError._code == NSURLErrorNotConnectedToInternet { return .failure(ParseError(code: .unknownError, message: "Unable to connect with parse-server: \(responseError)")) + } else { + guard let parseError = responseError as? ParseError else { + return .failure(ParseError(code: .notConnectedToInternet, + message: "Unable to connect with the internet: \(responseError)")) + } + return .failure(parseError) } - return .failure(parseError) } guard let response = urlResponse else { guard let parseError = responseError as? ParseError else { diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index 2667cde98..b2b999f47 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -348,6 +348,11 @@ public struct ParseError: ParseTypeable, Swift.Error { a non-2XX status code. */ case xDomainRequest = 602 + + /** + Error code indicating that the device is not connected to the internet. + */ + case notConnectedToInternet = 1009 /** Error code indicating any other custom error sent from the Parse Server. From f96380a7e8c746cf77918d52a215f51f2ab98c75 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:42:04 +0100 Subject: [PATCH 26/55] Compare error --- Sources/ParseSwift/Types/Query.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index dca903d78..788f8c905 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -551,7 +551,7 @@ extension Query: Queryable { return objects } catch let parseError { - if parseError.equalsTo(.connectionFailed), + if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { @@ -617,7 +617,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.connectionFailed), + if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { From 3acf50cc6f3a5415fe44939ce9510251e2067853 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 22:45:05 +0100 Subject: [PATCH 27/55] Fix --- Sources/ParseSwift/Extensions/URLSession.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 53f7e959e..30de02965 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -67,12 +67,12 @@ internal extension URLSession { mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { if responseError._code == NSURLErrorNotConnectedToInternet { - return .failure(ParseError(code: .unknownError, - message: "Unable to connect with parse-server: \(responseError)")) + return .failure(ParseError(code: .notConnectedToInternet, + message: "Unable to connect with the internet: \(responseError)")) } else { guard let parseError = responseError as? ParseError else { - return .failure(ParseError(code: .notConnectedToInternet, - message: "Unable to connect with the internet: \(responseError)")) + return .failure(ParseError(code: .unknownError, + message: "Unable to connect with parse-server: \(responseError)")) } return .failure(parseError) } From 087400074dcd51cebdad4eb3fb9f8da88281593b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 23:01:54 +0100 Subject: [PATCH 28/55] Try new error code for fix iOS --- Sources/ParseSwift/Extensions/URLSession.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 30de02965..216d72a75 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -66,7 +66,7 @@ internal extension URLSession { responseError: Error?, mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { - if responseError._code == NSURLErrorNotConnectedToInternet { + if let err = responseError as? URLError, err.code == URLError.Code.notConnectedToInternet { return .failure(ParseError(code: .notConnectedToInternet, message: "Unable to connect with the internet: \(responseError)")) } else { From 517499c4bbc478fd0d5e7ac77599d7238a94016e Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Wed, 21 Dec 2022 23:05:17 +0100 Subject: [PATCH 29/55] Connection not allowed --- Sources/ParseSwift/Extensions/URLSession.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Extensions/URLSession.swift b/Sources/ParseSwift/Extensions/URLSession.swift index 216d72a75..204a578c6 100644 --- a/Sources/ParseSwift/Extensions/URLSession.swift +++ b/Sources/ParseSwift/Extensions/URLSession.swift @@ -66,7 +66,8 @@ internal extension URLSession { responseError: Error?, mapper: @escaping (Data) throws -> U) -> Result { if let responseError = responseError { - if let err = responseError as? URLError, err.code == URLError.Code.notConnectedToInternet { + if let urlError = responseError as? URLError, + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed { return .failure(ParseError(code: .notConnectedToInternet, message: "Unable to connect with the internet: \(responseError)")) } else { From f0e9cee20761aaf2de4a285ef412296c199bb650 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 19:20:20 +0100 Subject: [PATCH 30/55] Function changes --- Sources/ParseSwift/Storage/LocalStorage.swift | 74 +++++++++++-------- Sources/ParseSwift/Types/Query.swift | 4 +- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index d8582b707..f7cb3b972 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -12,6 +12,7 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { let fileManager = FileManager.default + print("[LocalStorage] save object") let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { @@ -35,6 +36,7 @@ internal struct LocalStorage { static func save(_ objects: [T], queryIdentifier: String?) throws { let fileManager = FileManager.default + print("[LocalStorage] save objects") var successObjects: [T] = [] for object in objects { @@ -132,26 +134,32 @@ internal struct QueryObject: Codable { internal extension ParseObject { - func saveLocally(method: Method, + func saveLocally(method: Method? = nil, queryIdentifier: String? = nil) throws { - switch method { - case .save: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - } - case .create: - if Parse.configuration.offlinePolicy.canCreate { - if Parse.configuration.isRequiringCustomObjectIds { + if let method = method { + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + if Parse.configuration.isRequiringCustomObjectIds { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } else { + throw ParseError(code: .unknownError, message: "Enable custom objectIds") + } + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) - } else { - throw ParseError(code: .unknownError, message: "Enable custom objectIds") } } - case .replace: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - } - case .update: + } else { if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) } @@ -161,24 +169,30 @@ internal extension ParseObject { internal extension Sequence where Element: ParseObject { - func saveLocally(method: Method, + func saveLocally(method: Method? = nil, queryIdentifier: String? = nil) throws { let objects = map { $0 } - switch method { - case .save: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) - } - case .create: - if Parse.configuration.offlinePolicy.canCreate { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) - } - case .replace: - if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + if let method = method { + switch method { + case .save: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .create: + if Parse.configuration.offlinePolicy.canCreate { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .replace: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } + case .update: + if Parse.configuration.offlinePolicy.enabled { + try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + } } - case .update: + } else { if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(objects, queryIdentifier: queryIdentifier) } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 788f8c905..8853c559e 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -547,7 +547,7 @@ extension Query: Queryable { if useLocalStore { do { let objects = try findCommand().execute(options: options) - try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try? objects.saveLocally(queryIdentifier: queryIdentifier) return objects } catch let parseError { @@ -613,7 +613,7 @@ extension Query: Queryable { if useLocalStore { switch result { case .success(let objects): - try? LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try? objects.saveLocally(queryIdentifier: queryIdentifier) completion(result) case .failure(let failure): From 6b32d79507526e41d122d93c074043e05ec1ea9b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 19:54:38 +0100 Subject: [PATCH 31/55] FindAll --- Sources/ParseSwift/Types/Query.swift | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 8853c559e..62b895ca6 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -747,16 +747,30 @@ extension Query: Queryable { finished = true } } catch { - let defaultError = ParseError(code: .unknownError, - message: error.localizedDescription) - let parseError = error as? ParseError ?? defaultError - callbackQueue.async { - completion(.failure(parseError)) + if let urlError = error as? URLError, + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObjects)) + } else { + let defaultError = ParseError(code: .unknownError, + message: error.localizedDescription) + let parseError = error as? ParseError ?? defaultError + + if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObjects)) + } else { + callbackQueue.async { + completion(.failure(parseError)) + } + } } return } } - + + if useLocalStore { + try? results.saveLocally(queryIdentifier: queryIdentifier) + } callbackQueue.async { completion(.success(results)) } From 05d3821c843cadbb2041ee24ba29ae9953ad8610 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 20:20:32 +0100 Subject: [PATCH 32/55] First --- Sources/ParseSwift/Storage/LocalStorage.swift | 28 +++++++---- Sources/ParseSwift/Types/Query.swift | 46 ++++++++++++++++--- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index f7cb3b972..202c0ffac 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -33,7 +33,7 @@ internal struct LocalStorage { } } - static func save(_ objects: [T], + static func saveAll(_ objects: [T], queryIdentifier: String?) throws { let fileManager = FileManager.default print("[LocalStorage] save objects") @@ -63,14 +63,24 @@ internal struct LocalStorage { } static func get(_ type: U.Type, + queryIdentifier: String) throws -> U? { + guard let queryObjects = try getQueryObjects()[queryIdentifier], + let queryObject = queryObjects.first else { return nil } + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) + let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) + + let objectData = try Data(contentsOf: objectPath) + + return try ParseCoding.jsonDecoder().decode(U.self, from: objectData) + } + + static func getAll(_ type: U.Type, queryIdentifier: String) throws -> [U]? { - print("[LocalStorage] get objects") - print("[LocalStorage] queryIdentifier: \(String(describing: queryIdentifier))") guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] for queryObject in queryObjects { - print("[LocalStorage] get id: \(queryObject.objectId)") let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: queryObject.className) let objectPath = objectsDirectoryPath.appendingPathComponent(queryObject.objectId) @@ -177,24 +187,24 @@ internal extension Sequence where Element: ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } case .create: if Parse.configuration.offlinePolicy.canCreate { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } case .replace: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } case .update: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } } else { if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 62b895ca6..1f4337386 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -552,7 +552,7 @@ extension Query: Queryable { return objects } catch let parseError { if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { throw parseError @@ -618,7 +618,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { completion(.failure(failure)) @@ -748,7 +748,7 @@ extension Query: Queryable { } } catch { if let urlError = error as? URLError, - urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + urlError.code == URLError.Code.notConnectedToInternet || urlError.code == URLError.Code.dataNotAllowed, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { let defaultError = ParseError(code: .unknownError, @@ -756,7 +756,7 @@ extension Query: Queryable { let parseError = error as? ParseError ?? defaultError if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), - let localObjects = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { callbackQueue.async { @@ -791,7 +791,23 @@ extension Query: Queryable { throw ParseError(code: .objectNotFound, message: "Object not found on the server.") } - return try firstCommand().execute(options: options) + if useLocalStore { + do { + let objects = try firstCommand().execute(options: options) + try? objects.saveLocally(queryIdentifier: queryIdentifier) + + return objects + } catch let parseError { + if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + return localObject + } else { + throw parseError + } + } + } else { + return try firstCommand().execute(options: options) + } } /** @@ -846,8 +862,24 @@ extension Query: Queryable { } do { try firstCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in - completion(result) + callbackQueue: callbackQueue) { result in + if useLocalStore { + switch result { + case .success(let object): + try? object.saveLocally(queryIdentifier: queryIdentifier) + + completion(result) + case .failure(let failure): + if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), + let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { + completion(.success(localObject)) + } else { + completion(.failure(failure)) + } + } + } else { + completion(result) + } } } catch { let parseError = ParseError(code: .unknownError, From 0e4410937dea98548409683ad3487c6659bb7d79 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 20:38:33 +0100 Subject: [PATCH 33/55] Save object --- Sources/ParseSwift/Objects/ParseObject+async.swift | 4 ++++ Sources/ParseSwift/Objects/ParseObject.swift | 14 ++++++-------- Sources/ParseSwift/Storage/LocalStorage.swift | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index de7f30c4b..f8be75483 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -378,6 +378,8 @@ or disable transactions for this call. case .update: command = try self.updateCommand() } + + try? saveLocally(method: method) return try await command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -387,6 +389,8 @@ or disable transactions for this call. let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError + + try? saveLocally(method: method, error: parseError) throw parseError } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 0ca0fbe60..91948a9ff 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1209,7 +1209,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, @@ -1226,7 +1225,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, options: options, @@ -1252,7 +1250,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1267,7 +1264,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1292,7 +1288,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1307,7 +1302,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1332,7 +1326,6 @@ extension ParseObject { #if compiler(>=5.5.2) && canImport(_Concurrency) Task { do { - try? saveLocally(method: method) let object = try await command(method: method, options: options, callbackQueue: callbackQueue) @@ -1347,7 +1340,6 @@ extension ParseObject { } } #else - try? saveLocally(method: method) command(method: method, options: options, callbackQueue: callbackQueue, @@ -1374,6 +1366,8 @@ extension ParseObject { case .update: command = try self.updateCommand() } + + try? saveLocally(method: method) command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -1384,12 +1378,16 @@ extension ParseObject { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError + + try? saveLocally(method: method, error: parseError) callbackQueue.async { completion(.failure(parseError)) } } return } + + try? saveLocally(method: method, error: parseError) callbackQueue.async { completion(.failure(parseError)) } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 202c0ffac..bff698c69 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -12,7 +12,6 @@ internal struct LocalStorage { static func save(_ object: T, queryIdentifier: String?) throws { let fileManager = FileManager.default - print("[LocalStorage] save object") let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { @@ -36,7 +35,6 @@ internal struct LocalStorage { static func saveAll(_ objects: [T], queryIdentifier: String?) throws { let fileManager = FileManager.default - print("[LocalStorage] save objects") var successObjects: [T] = [] for object in objects { @@ -145,7 +143,8 @@ internal struct QueryObject: Codable { internal extension ParseObject { func saveLocally(method: Method? = nil, - queryIdentifier: String? = nil) throws { + queryIdentifier: String? = nil, + error: ParseError? = nil) throws { if let method = method { switch method { case .save: @@ -180,7 +179,8 @@ internal extension ParseObject { internal extension Sequence where Element: ParseObject { func saveLocally(method: Method? = nil, - queryIdentifier: String? = nil) throws { + queryIdentifier: String? = nil, + error: ParseError? = nil) throws { let objects = map { $0 } if let method = method { From 471863c064bd123263509b5c2045b129c9c1c750 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 23:13:57 +0100 Subject: [PATCH 34/55] saveLocally --- Sources/ParseSwift/Objects/ParseObject+async.swift | 4 ++++ Sources/ParseSwift/Objects/ParseObject.swift | 2 ++ Sources/ParseSwift/Types/ParseError.swift | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index f8be75483..6c0ed95eb 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -462,11 +462,15 @@ internal extension Sequence where Element: ParseObject { childFiles: childFiles) returnBatch.append(contentsOf: saved) } + + try? saveLocally(method: method) return returnBatch } catch { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError + + try? saveLocally(method: method, error: parseError) throw parseError } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 91948a9ff..b3150afb6 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -800,10 +800,12 @@ transactions for this call. case .success(let saved): returnBatch.append(contentsOf: saved) if completed == (batches.count - 1) { + try? saveLocally(method: method) completion(.success(returnBatch)) } completed += 1 case .failure(let error): + try? saveLocally(method: method, error: error) completion(.failure(error)) return } diff --git a/Sources/ParseSwift/Types/ParseError.swift b/Sources/ParseSwift/Types/ParseError.swift index b2b999f47..1ce4d4c9b 100644 --- a/Sources/ParseSwift/Types/ParseError.swift +++ b/Sources/ParseSwift/Types/ParseError.swift @@ -563,3 +563,15 @@ public extension Error { containedIn(errorCodes) } } + +internal extension Error { + + /** + Validates if the given `ParseError` codes contains the error codes for no internet connection. + + - returns: A boolean indicating whether or not the `Error` is an internet connection error. + */ + var hasNoInternetConnection: Bool { + return self.equalsTo(.notConnectedToInternet) || self.equalsTo(.connectionFailed) + } +} From 56c63432b6a5255389d77d12a714cde91cb0786f Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Thu, 22 Dec 2022 23:15:11 +0100 Subject: [PATCH 35/55] hasNoInternetConnection --- Sources/ParseSwift/Types/Query.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 1f4337386..01ca9f26e 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -551,7 +551,7 @@ extension Query: Queryable { return objects } catch let parseError { - if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + if parseError.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { return localObjects } else { @@ -617,7 +617,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), + if failure.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { @@ -755,7 +755,7 @@ extension Query: Queryable { message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + if parseError.hasNoInternetConnection, let localObjects = try? LocalStorage.getAll(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObjects)) } else { @@ -798,7 +798,7 @@ extension Query: Queryable { return objects } catch let parseError { - if parseError.equalsTo(.notConnectedToInternet) || parseError.equalsTo(.connectionFailed), + if parseError.hasNoInternetConnection, let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { return localObject } else { @@ -870,7 +870,7 @@ extension Query: Queryable { completion(result) case .failure(let failure): - if failure.equalsTo(.notConnectedToInternet) || failure.equalsTo(.connectionFailed), + if failure.hasNoInternetConnection, let localObject = try? LocalStorage.get(ResultType.self, queryIdentifier: queryIdentifier) { completion(.success(localObject)) } else { From 6ac8a93332806e9ada17a0eb8600730ffbfdb10e Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 00:55:45 +0100 Subject: [PATCH 36/55] FetchObject --- Sources/ParseSwift/ParseConstants.swift | 3 +- Sources/ParseSwift/Storage/LocalStorage.swift | 60 ++++++++++++++++++- Sources/ParseSwift/Types/Query.swift | 2 +- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index a86679262..15b56842c 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -16,6 +16,7 @@ enum ParseConstants { static let fileManagementLibraryDirectory = "Library/" static let fileDownloadsDirectory = "Downloads" static let fileObjectsDirectory = "Objects" + static let fetchObjectsFile = "FetchObjects" static let queryObjectsFile = "QueryObjects" static let bundlePrefix = "com.parse.ParseSwift" static let batchLimit = 50 @@ -37,7 +38,7 @@ enum ParseConstants { #endif } -enum Method: String { +enum Method: String, Codable { case save, create, replace, update } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index bff698c69..0b1710a33 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -91,6 +91,39 @@ internal struct LocalStorage { return (allObjects.isEmpty ? nil : allObjects) } + static func saveFetchObjects(_ objects: [T], + method: Method) throws { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + + var fetchObjects = try getFetchObjects() + fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) + + let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) + + if fileManager.fileExists(atPath: fetchObjectsPath.path) { + try jsonData.write(to: fetchObjectsPath) + } else { + fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil) + } + } + + static func getFetchObjects() throws -> [FetchObject] { + let fileManager = FileManager.default + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + + if fileManager.fileExists(atPath: fetchObjectsPath.path) { + let jsonData = try Data(contentsOf: fetchObjectsPath) + return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData) + } else { + return [] + } + } + static func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { let fileManager = FileManager.default @@ -125,6 +158,23 @@ internal struct LocalStorage { } } +internal struct FetchObject: Codable { + let objectId: String + let className: String + let updatedAt: Date + let method: Method + + init(_ object : T, method: Method) throws { + guard let objectId = object.objectId else { + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") + } + self.objectId = objectId + self.className = object.className + self.updatedAt = object.updatedAt ?? Date() + self.method = method + } +} + internal struct QueryObject: Codable { let objectId: String let className: String @@ -132,7 +182,7 @@ internal struct QueryObject: Codable { init(_ object : T) throws { guard let objectId = object.objectId else { - throw ParseError(code: .unknownError, message: "Object has no valid objectId") + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } self.objectId = objectId self.className = object.className @@ -173,6 +223,10 @@ internal extension ParseObject { try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } + + if let method = method, let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } } @@ -207,5 +261,9 @@ internal extension Sequence where Element: ParseObject { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } + + if let method = method, let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } } diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 01ca9f26e..baec0e92e 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -862,7 +862,7 @@ extension Query: Queryable { } do { try firstCommand().executeAsync(options: options, - callbackQueue: callbackQueue) { result in + callbackQueue: callbackQueue) { result in if useLocalStore { switch result { case .success(let object): From 6d7d95df4615c7e7e0bc54113d324d76a08263e2 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 01:08:00 +0100 Subject: [PATCH 37/55] HiddenFile --- Sources/ParseSwift/Storage/LocalStorage.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 0b1710a33..d27c3efa1 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -96,7 +96,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) @@ -114,7 +114,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile) + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) @@ -129,7 +129,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) @@ -147,7 +147,7 @@ internal struct LocalStorage { let fileManager = FileManager.default let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile) + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) if fileManager.fileExists(atPath: queryObjectsPath.path) { let jsonData = try Data(contentsOf: queryObjectsPath) @@ -158,6 +158,16 @@ internal struct LocalStorage { } } +fileprivate extension String { + + /** + Creates a hidden file + */ + var hiddenFile: Self { + return "." + self + } +} + internal struct FetchObject: Codable { let objectId: String let className: String From 1059b3bab99ecb4757e48e53956b009bbdeca25d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:43:00 +0100 Subject: [PATCH 38/55] uniqueObjectsById, Create fetchobjects when policy enabled --- Sources/ParseSwift/Storage/LocalStorage.swift | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index d27c3efa1..28b1741a8 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -118,7 +118,7 @@ internal struct LocalStorage { if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) - return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData) + return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById } else { return [] } @@ -210,11 +210,19 @@ internal extension ParseObject { case .save: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } case .create: if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } else { throw ParseError(code: .unknownError, message: "Enable custom objectIds") } @@ -222,10 +230,18 @@ internal extension ParseObject { case .replace: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } case .update: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.save(self, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects([self], method: method) + } } } } else { @@ -233,10 +249,6 @@ internal extension ParseObject { try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } - - if let method = method, let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) - } } } @@ -252,18 +264,38 @@ internal extension Sequence where Element: ParseObject { case .save: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } case .create: if Parse.configuration.offlinePolicy.canCreate { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + if Parse.configuration.isRequiringCustomObjectIds { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + throw ParseError(code: .unknownError, message: "Enable custom objectIds") + } } case .replace: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } case .update: if Parse.configuration.offlinePolicy.enabled { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + + if let error = error, error.hasNoInternetConnection { + try LocalStorage.saveFetchObjects(objects, method: method) + } } } } else { @@ -271,9 +303,19 @@ internal extension Sequence where Element: ParseObject { try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } + } +} + +fileprivate extension Sequence where Element == FetchObject { + + var uniqueObjectsById: [Element] { + let objects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) - if let method = method, let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + var uniqueObjects: [Element] = [] + for object in objects { + uniqueObjects.append(objects.first(where: { $0.objectId == object.objectId }) ?? object) } + + return uniqueObjects.isEmpty ? objects : uniqueObjects } } From 799ed424c63e6c46a9411b145f2f1c90615c734d Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:45:19 +0100 Subject: [PATCH 39/55] Cleanup --- Sources/ParseSwift/Storage/LocalStorage.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 28b1741a8..ef1fe35bc 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -8,10 +8,10 @@ import Foundation internal struct LocalStorage { + static let fileManager = FileManager.default static func save(_ object: T, queryIdentifier: String?) throws { - let fileManager = FileManager.default let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { @@ -34,8 +34,6 @@ internal struct LocalStorage { static func saveAll(_ objects: [T], queryIdentifier: String?) throws { - let fileManager = FileManager.default - var successObjects: [T] = [] for object in objects { let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -93,8 +91,6 @@ internal struct LocalStorage { static func saveFetchObjects(_ objects: [T], method: Method) throws { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) @@ -111,8 +107,6 @@ internal struct LocalStorage { } static func getFetchObjects() throws -> [FetchObject] { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) @@ -126,8 +120,6 @@ internal struct LocalStorage { static func saveQueryObjects(_ objects: [T], queryIdentifier: String) throws { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) @@ -144,8 +136,6 @@ internal struct LocalStorage { } static func getQueryObjects() throws -> [String : [QueryObject]] { - let fileManager = FileManager.default - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) From 345993ad152cd56af714a55f514855f33acd29ea Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:47:20 +0100 Subject: [PATCH 40/55] Comment --- Sources/ParseSwift/Storage/ParseFileManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ParseSwift/Storage/ParseFileManager.swift b/Sources/ParseSwift/Storage/ParseFileManager.swift index 81fc3609b..b8b3fab85 100644 --- a/Sources/ParseSwift/Storage/ParseFileManager.swift +++ b/Sources/ParseSwift/Storage/ParseFileManager.swift @@ -230,6 +230,7 @@ public extension ParseFileManager { /** The default directory for all `ParseObject`'s. + - parameter className: An optional value, that if set returns the objects directory for a specific class - returns: The objects directory. - throws: An error of type `ParseError`. */ From 4fb6971e9fc808473adb6f0ba45f734647721bd8 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:48:30 +0100 Subject: [PATCH 41/55] Moved some code --- Sources/ParseSwift/Storage/LocalStorage.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index ef1fe35bc..4a9dcf49b 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -148,16 +148,6 @@ internal struct LocalStorage { } } -fileprivate extension String { - - /** - Creates a hidden file - */ - var hiddenFile: Self { - return "." + self - } -} - internal struct FetchObject: Codable { let objectId: String let className: String @@ -296,6 +286,16 @@ internal extension Sequence where Element: ParseObject { } } +fileprivate extension String { + + /** + Creates a hidden file + */ + var hiddenFile: Self { + return "." + self + } +} + fileprivate extension Sequence where Element == FetchObject { var uniqueObjectsById: [Element] { From 158a151a6b7469490e4e98d4519d4e1ece2d95cd Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Fri, 23 Dec 2022 11:57:48 +0100 Subject: [PATCH 42/55] Make sure new FetchObjects are unique --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 4a9dcf49b..4b1b1bc9f 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -96,6 +96,7 @@ internal struct LocalStorage { var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) + fetchObjects = fetchObjects.uniqueObjectsById let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) @@ -298,6 +299,9 @@ fileprivate extension String { fileprivate extension Sequence where Element == FetchObject { + /** + Returns a unique array of `FetchObject`'s where each element is the most recent version of itself. + */ var uniqueObjectsById: [Element] { let objects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) From f3566eb286156664dec2de8f068b0ce9364384ef Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sat, 24 Dec 2022 01:04:11 +0100 Subject: [PATCH 43/55] Fetching Local Store --- .../Objects/ParseObject+async.swift | 48 ++++-- Sources/ParseSwift/Objects/ParseObject.swift | 68 +++++++- Sources/ParseSwift/Storage/LocalStorage.swift | 152 +++++++++++++++++- 3 files changed, 243 insertions(+), 25 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 6c0ed95eb..8b0e8e111 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -42,9 +42,11 @@ public extension ParseObject { - throws: An error of type `ParseError`. */ @discardableResult func save(ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in self.save(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -56,9 +58,11 @@ public extension ParseObject { - returns: Returns the saved `ParseObject`. - throws: An error of type `ParseError`. */ - @discardableResult func create(options: API.Options = []) async throws -> Self { + @discardableResult func create(ignoringLocalStore: Bool = false, + options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - self.create(options: options, + self.create(ignoringLocalStore: ignoringLocalStore, + options: options, completion: continuation.resume) } } @@ -69,9 +73,11 @@ public extension ParseObject { - returns: Returns the saved `ParseObject`. - throws: An error of type `ParseError`. */ - @discardableResult func replace(options: API.Options = []) async throws -> Self { + @discardableResult func replace(ignoringLocalStore: Bool = false, + options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - self.replace(options: options, + self.replace(ignoringLocalStore: ignoringLocalStore, + options: options, completion: continuation.resume) } } @@ -81,10 +87,12 @@ public extension ParseObject { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns the saved `ParseObject`. - throws: An error of type `ParseError`. - */ - @discardableResult internal func update(options: API.Options = []) async throws -> Self { + */ + @discardableResult internal func update(ignoringLocalStore: Bool = false, + options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - self.update(options: options, + self.update(ignoringLocalStore: ignoringLocalStore, + options: options, completion: continuation.resume) } } @@ -159,11 +167,13 @@ public extension Sequence where Element: ParseObject { @discardableResult func saveAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.saveAll(batchLimit: limit, transaction: transaction, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -188,10 +198,12 @@ public extension Sequence where Element: ParseObject { */ @discardableResult func createAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.createAll(batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -216,10 +228,12 @@ public extension Sequence where Element: ParseObject { */ @discardableResult func replaceAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.replaceAll(batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -244,10 +258,12 @@ public extension Sequence where Element: ParseObject { */ internal func updateAll(batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = []) async throws -> [(Result)] { try await withCheckedThrowingContinuation { continuation in self.updateAll(batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, completion: continuation.resume) } @@ -363,6 +379,7 @@ or disable transactions for this call. func command(method: Method, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue) async throws -> Self { let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options) @@ -379,7 +396,9 @@ or disable transactions for this call. command = try self.updateCommand() } - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } return try await command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -390,7 +409,9 @@ or disable transactions for this call. message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } throw parseError } } @@ -402,6 +423,7 @@ internal extension Sequence where Element: ParseObject { batchLimit limit: Int?, transaction: Bool, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue) async throws -> [(Result)] { var options = options @@ -463,14 +485,18 @@ internal extension Sequence where Element: ParseObject { returnBatch.append(contentsOf: saved) } - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } return returnBatch } catch { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } throw parseError } } diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index b3150afb6..7c8aa016a 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -478,6 +478,8 @@ transactions for this call. - parameter ignoringCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.isRequiringCustomObjectIds = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -501,6 +503,7 @@ transactions for this call. batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -513,6 +516,7 @@ transactions for this call. batchLimit: limit, transaction: transaction, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -530,6 +534,7 @@ transactions for this call. batchLimit: limit, transaction: transaction, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -543,6 +548,8 @@ transactions for this call. Defaults to 50. - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that prevents the transaction from completing, then none of the objects are committed to the Parse Server database. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -556,6 +563,7 @@ transactions for this call. func createAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -567,6 +575,7 @@ transactions for this call. let objects = try await batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -583,6 +592,7 @@ transactions for this call. batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -596,6 +606,8 @@ transactions for this call. Defaults to 50. - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that prevents the transaction from completing, then none of the objects are committed to the Parse Server database. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -609,6 +621,7 @@ transactions for this call. func replaceAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -620,6 +633,7 @@ transactions for this call. let objects = try await batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -636,6 +650,7 @@ transactions for this call. batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -649,6 +664,8 @@ transactions for this call. Defaults to 50. - parameter transaction: Treat as an all-or-nothing operation. If some operation failure occurs that prevents the transaction from completing, then none of the objects are committed to the Parse Server database. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -662,6 +679,7 @@ transactions for this call. internal func updateAll( // swiftlint:disable:this function_body_length cyclomatic_complexity batchLimit limit: Int? = nil, transaction: Bool = configuration.isUsingTransactions, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[(Result)], ParseError>) -> Void @@ -673,6 +691,7 @@ transactions for this call. let objects = try await batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(objects)) @@ -689,6 +708,7 @@ transactions for this call. batchCommand(method: method, batchLimit: limit, transaction: transaction, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -699,6 +719,7 @@ transactions for this call. batchLimit limit: Int?, transaction: Bool, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue, completion: @escaping (Result<[(Result)], ParseError>) -> Void) { @@ -800,12 +821,16 @@ transactions for this call. case .success(let saved): returnBatch.append(contentsOf: saved) if completed == (batches.count - 1) { - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } completion(.success(returnBatch)) } completed += 1 case .failure(let error): - try? saveLocally(method: method, error: error) + if !ignoringLocalStore { + try? saveLocally(method: method, error: error) + } completion(.failure(error)) return } @@ -1187,6 +1212,8 @@ extension ParseObject { - parameter ignoringCustomObjectIdConfig: Ignore checking for `objectId` when `ParseConfiguration.isRequiringCustomObjectIds = true` to allow for mixed `objectId` environments. Defaults to false. + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. @@ -1203,6 +1230,7 @@ extension ParseObject { */ public func save( ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1213,6 +1241,7 @@ extension ParseObject { do { let object = try await command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) @@ -1229,6 +1258,7 @@ extension ParseObject { #else command(method: method, ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1237,13 +1267,16 @@ extension ParseObject { /** Creates the `ParseObject` *asynchronously* and executes the given callback block. - + + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ public func create( + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1253,6 +1286,7 @@ extension ParseObject { Task { do { let object = try await command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(object)) @@ -1267,6 +1301,7 @@ extension ParseObject { } #else command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1275,13 +1310,16 @@ extension ParseObject { /** Replaces the `ParseObject` *asynchronously* and executes the given callback block. - + + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ public func replace( + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1291,6 +1329,7 @@ extension ParseObject { Task { do { let object = try await command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(object)) @@ -1305,6 +1344,7 @@ extension ParseObject { } #else command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1313,13 +1353,16 @@ extension ParseObject { /** Updates the `ParseObject` *asynchronously* and executes the given callback block. - + + - parameter ignoringLocalStore: Ignore storing objects to the local store. Mainly used for internal use + since default use should be set via policy on initialize. Defaults to false. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ func update( + ignoringLocalStore: Bool = false, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void @@ -1329,6 +1372,7 @@ extension ParseObject { Task { do { let object = try await command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue) completion(.success(object)) @@ -1343,6 +1387,7 @@ extension ParseObject { } #else command(method: method, + ignoringLocalStore: ignoringLocalStore, options: options, callbackQueue: callbackQueue, completion: completion) @@ -1351,6 +1396,7 @@ extension ParseObject { func command(method: Method, ignoringCustomObjectIdConfig: Bool = false, + ignoringLocalStore: Bool = false, options: API.Options, callbackQueue: DispatchQueue, completion: @escaping (Result) -> Void) { @@ -1369,7 +1415,9 @@ extension ParseObject { command = try self.updateCommand() } - try? saveLocally(method: method) + if !ignoringLocalStore { + try? saveLocally(method: method) + } command .executeAsync(options: options, callbackQueue: callbackQueue, @@ -1381,7 +1429,9 @@ extension ParseObject { message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } callbackQueue.async { completion(.failure(parseError)) } @@ -1389,7 +1439,9 @@ extension ParseObject { return } - try? saveLocally(method: method, error: parseError) + if !ignoringLocalStore { + try? saveLocally(method: method, error: parseError) + } callbackQueue.async { completion(.failure(parseError)) } diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 4b1b1bc9f..1da9dff69 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -7,6 +7,21 @@ import Foundation +public extension ParseObject { + + /** + Fetch all local objects. + + - returns: If objects are more recent on the database, it will replace the local objects and return them. + + - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects + after creating offline objects. + */ + @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { + return try await LocalStorage.fetchLocalObjects(type) + } +} + internal struct LocalStorage { static let fileManager = FileManager.default @@ -147,6 +162,94 @@ internal struct LocalStorage { return [:] } } + + /** + Fetch all local objects. + + - returns: If objects are more recent on the database, it will replace the local objects and return them. + */ + @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { + let fetchObjects = try getFetchObjects() + if fetchObjects.isEmpty { + return nil + } + + var saveObjects = try fetchObjects + .filter({ $0.method == .save }) + .asParseObjects(type) + var createObjects = try fetchObjects + .filter({ $0.method == .create }) + .asParseObjects(type) + var replaceObjects = try fetchObjects + .filter({ $0.method == .replace }) + .asParseObjects(type) + var updateObjects = try fetchObjects + .filter({ $0.method == .update }) + .asParseObjects(type) + + var cloudObjects: [T] = [] + + if Parse.configuration.offlinePolicy.enabled { + try await self.fetchLocalStore(.save, objects: &saveObjects, cloudObjects: &cloudObjects) + } + + if Parse.configuration.offlinePolicy.canCreate { + if Parse.configuration.isRequiringCustomObjectIds { + try await self.fetchLocalStore(.create, objects: &createObjects, cloudObjects: &cloudObjects) + } else { + assertionFailure("Enable custom objectIds") + } + } + + if Parse.configuration.offlinePolicy.enabled { + try await self.fetchLocalStore(.replace, objects: &replaceObjects, cloudObjects: &cloudObjects) + } + + if Parse.configuration.offlinePolicy.enabled { + try await self.fetchLocalStore(.update, objects: &updateObjects, cloudObjects: &cloudObjects) + } + + if cloudObjects.isEmpty { + return nil + } else { + try self.saveAll(cloudObjects, queryIdentifier: nil) + return cloudObjects + } + } + + private static func fetchLocalStore(_ method: Method, objects: inout [T], cloudObjects: inout [T]) async throws { + let queryObjects = T.query() + .where(containedIn(key: "objectId", array: objects.map({ $0.objectId }))) + .useLocalStore(false) + let foundObjects = try? await queryObjects.find() + + for object in objects { + if let matchingObject = foundObjects?.first(where: { $0.objectId == object.objectId }) { + if let objectUpdatedAt = object.updatedAt { + if let matchingObjectUpdatedAt = matchingObject.updatedAt { + if objectUpdatedAt < matchingObjectUpdatedAt { + objects.removeAll(where: { $0.objectId == matchingObject.objectId }) + cloudObjects.append(matchingObject) + } + } + } else { + objects.removeAll(where: { $0.objectId == matchingObject.objectId }) + cloudObjects.append(matchingObject) + } + } + } + + switch method { + case .save: + try await objects.saveAll(ignoringLocalStore: true) + case .create: + try await objects.createAll(ignoringLocalStore: true) + case .replace: + try await objects.replaceAll(ignoringLocalStore: true) + case .update: + _ = try await objects.updateAll(ignoringLocalStore: true) + } + } } internal struct FetchObject: Codable { @@ -205,7 +308,7 @@ internal extension ParseObject { try LocalStorage.saveFetchObjects([self], method: method) } } else { - throw ParseError(code: .unknownError, message: "Enable custom objectIds") + assertionFailure("Enable custom objectIds") } } case .replace: @@ -259,7 +362,7 @@ internal extension Sequence where Element: ParseObject { try LocalStorage.saveFetchObjects(objects, method: method) } } else { - throw ParseError(code: .unknownError, message: "Enable custom objectIds") + assertionFailure("Enable custom objectIds") } } case .replace: @@ -303,13 +406,50 @@ fileprivate extension Sequence where Element == FetchObject { Returns a unique array of `FetchObject`'s where each element is the most recent version of itself. */ var uniqueObjectsById: [Element] { - let objects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) + let fetchObjects = map { $0 }.sorted(by: { $0.updatedAt > $1.updatedAt }) var uniqueObjects: [Element] = [] - for object in objects { - uniqueObjects.append(objects.first(where: { $0.objectId == object.objectId }) ?? object) + for fetchObject in fetchObjects { + uniqueObjects.append(fetchObjects.first(where: { $0.objectId == fetchObject.objectId }) ?? fetchObject) + } + + return uniqueObjects.isEmpty ? fetchObjects : uniqueObjects + } + + func asParseObjects(_ type: T.Type) throws -> [T] { + let fileManager = FileManager.default + + let fetchObjectIds = map { $0 }.filter({ $0.className == T.className }).map({ $0.objectId }) + + let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: T.className) + let directoryObjectIds = try fileManager.contentsOfDirectory(atPath: objectsDirectoryPath.path) + + var objects: [T] = [] + + for directoryObjectId in directoryObjectIds { + if fetchObjectIds.contains(directoryObjectId) { + if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { + let contentPath = objectsDirectoryPath.appending(component: directoryObjectId, directoryHint: .notDirectory) + + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + + objects.append(object) + } + } else { + let contentPath = objectsDirectoryPath.appendingPathComponent(directoryObjectId, isDirectory: false) + + if fileManager.fileExists(atPath: contentPath.path) { + let jsonData = try Data(contentsOf: contentPath) + let object = try ParseCoding.jsonDecoder().decode(T.self, from: jsonData) + + objects.append(object) + } + } + } } - return uniqueObjects.isEmpty ? objects : uniqueObjects + return objects } } From 5ba3892380cb39028d871388e0f9a0a6085767de Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 12:06:53 +0100 Subject: [PATCH 44/55] Print --- Sources/ParseSwift/Objects/ParseObject+async.swift | 5 +++++ Sources/ParseSwift/Objects/ParseObject.swift | 1 + 2 files changed, 6 insertions(+) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 8b0e8e111..24c3804b2 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -383,6 +383,7 @@ or disable transactions for this call. options: API.Options, callbackQueue: DispatchQueue) async throws -> Self { let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options) + print("1") do { let command: API.Command! switch method { @@ -396,7 +397,9 @@ or disable transactions for this call. command = try self.updateCommand() } + print("2") if !ignoringLocalStore { + print("3") try? saveLocally(method: method) } return try await command @@ -405,11 +408,13 @@ or disable transactions for this call. childObjects: savedChildObjects, childFiles: savedChildFiles) } catch { + print("4") let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError if !ignoringLocalStore { + print("5") try? saveLocally(method: method, error: parseError) } throw parseError diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 7c8aa016a..8c98d9635 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1235,6 +1235,7 @@ extension ParseObject { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void ) { + print("01") let method = Method.save #if compiler(>=5.5.2) && canImport(_Concurrency) Task { From 0e258f44f53e02ca59a7b49936080e7116e1adf3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 12:22:14 +0100 Subject: [PATCH 45/55] Error change --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 1da9dff69..c7853a09d 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -30,7 +30,7 @@ internal struct LocalStorage { let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { - throw ParseError(code: .unknownError, message: "Object has no valid objectId") + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) @@ -53,7 +53,7 @@ internal struct LocalStorage { for object in objects { let objectData = try ParseCoding.jsonEncoder().encode(object) guard let objectId = object.objectId else { - throw ParseError(code: .unknownError, message: "Object has no valid objectId") + throw ParseError(code: .missingObjectId, message: "Object has no valid objectId") } let objectsDirectoryPath = try ParseFileManager.objectsDirectory(className: object.className) From 2c0dbf1acee4e54af69fbcc90441147e0a870785 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 16:20:11 +0100 Subject: [PATCH 46/55] Change function --- Sources/ParseSwift/Storage/LocalStorage.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index c7853a09d..bb005eb51 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -17,8 +17,8 @@ public extension ParseObject { - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects after creating offline objects. */ - @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { - return try await LocalStorage.fetchLocalObjects(type) + @discardableResult static func fetchLocalStore() async throws -> [T]? { + return try await LocalStorage.fetchLocalObjects() } } @@ -168,7 +168,7 @@ internal struct LocalStorage { - returns: If objects are more recent on the database, it will replace the local objects and return them. */ - @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { + @discardableResult static func fetchLocalObjects() async throws -> [T]? { let fetchObjects = try getFetchObjects() if fetchObjects.isEmpty { return nil @@ -176,16 +176,16 @@ internal struct LocalStorage { var saveObjects = try fetchObjects .filter({ $0.method == .save }) - .asParseObjects(type) + .asParseObjects(T.self) var createObjects = try fetchObjects .filter({ $0.method == .create }) - .asParseObjects(type) + .asParseObjects(T.self) var replaceObjects = try fetchObjects .filter({ $0.method == .replace }) - .asParseObjects(type) + .asParseObjects(T.self) var updateObjects = try fetchObjects .filter({ $0.method == .update }) - .asParseObjects(type) + .asParseObjects(T.self) var cloudObjects: [T] = [] From daa251d25f4eea5a8b341c13bdd8bcf90a137e96 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 16:26:47 +0100 Subject: [PATCH 47/55] Revert --- Sources/ParseSwift/Storage/LocalStorage.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index bb005eb51..c7853a09d 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -17,8 +17,8 @@ public extension ParseObject { - note: You will need to run this on every `ParseObject` that needs to fetch it's local objects after creating offline objects. */ - @discardableResult static func fetchLocalStore() async throws -> [T]? { - return try await LocalStorage.fetchLocalObjects() + @discardableResult static func fetchLocalStore(_ type: T.Type) async throws -> [T]? { + return try await LocalStorage.fetchLocalObjects(type) } } @@ -168,7 +168,7 @@ internal struct LocalStorage { - returns: If objects are more recent on the database, it will replace the local objects and return them. */ - @discardableResult static func fetchLocalObjects() async throws -> [T]? { + @discardableResult static func fetchLocalObjects(_ type: T.Type) async throws -> [T]? { let fetchObjects = try getFetchObjects() if fetchObjects.isEmpty { return nil @@ -176,16 +176,16 @@ internal struct LocalStorage { var saveObjects = try fetchObjects .filter({ $0.method == .save }) - .asParseObjects(T.self) + .asParseObjects(type) var createObjects = try fetchObjects .filter({ $0.method == .create }) - .asParseObjects(T.self) + .asParseObjects(type) var replaceObjects = try fetchObjects .filter({ $0.method == .replace }) - .asParseObjects(T.self) + .asParseObjects(type) var updateObjects = try fetchObjects .filter({ $0.method == .update }) - .asParseObjects(T.self) + .asParseObjects(type) var cloudObjects: [T] = [] From 3bfce97c3d8e8437441c2d904500481643979ec3 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 16:54:25 +0100 Subject: [PATCH 48/55] Lint fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index c7853a09d..584be6a74 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -48,7 +48,7 @@ internal struct LocalStorage { } static func saveAll(_ objects: [T], - queryIdentifier: String?) throws { + queryIdentifier: String?) throws { var successObjects: [T] = [] for object in objects { let objectData = try ParseCoding.jsonEncoder().encode(object) @@ -87,7 +87,7 @@ internal struct LocalStorage { } static func getAll(_ type: U.Type, - queryIdentifier: String) throws -> [U]? { + queryIdentifier: String) throws -> [U]? { guard let queryObjects = try getQueryObjects()[queryIdentifier] else { return nil } var allObjects: [U] = [] From 04826e07c9e01842dfc78563268c58c0c9ce6422 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Sun, 25 Dec 2022 18:04:58 +0100 Subject: [PATCH 49/55] Remove files if corrupted, Remove files if empty, code clean --- Sources/ParseSwift/Storage/LocalStorage.swift | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 584be6a74..191ff5dff 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -104,7 +104,7 @@ internal struct LocalStorage { return (allObjects.isEmpty ? nil : allObjects) } - static func saveFetchObjects(_ objects: [T], + static fileprivate func saveFetchObjects(_ objects: [T], method: Method) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) @@ -113,53 +113,95 @@ internal struct LocalStorage { fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) fetchObjects = fetchObjects.uniqueObjectsById - let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) + try self.writeFetchObjects(fetchObjects) + } + + static fileprivate func removeFetchObjects(_ objects: [T]) throws { + var fetchObjects = try getFetchObjects() + let objectIds = objects.compactMap({ $0.objectId }) + fetchObjects.removeAll(where: { removableObject in + objectIds.contains(where: { currentObjectId in + removableObject.objectId == currentObjectId + }) + }) + fetchObjects = fetchObjects.uniqueObjectsById - if fileManager.fileExists(atPath: fetchObjectsPath.path) { - try jsonData.write(to: fetchObjectsPath) - } else { - fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil) - } + try self.writeFetchObjects(fetchObjects) } - static func getFetchObjects() throws -> [FetchObject] { + static fileprivate func getFetchObjects() throws -> [FetchObject] { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) if fileManager.fileExists(atPath: fetchObjectsPath.path) { let jsonData = try Data(contentsOf: fetchObjectsPath) - return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById + do { + return try ParseCoding.jsonDecoder().decode([FetchObject].self, from: jsonData).uniqueObjectsById + } catch { + try fileManager.removeItem(at: fetchObjectsPath) + return [] + } } else { return [] } } - static func saveQueryObjects(_ objects: [T], - queryIdentifier: String) throws { + static private func writeFetchObjects(_ fetchObjects: [FetchObject]) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) + let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) + if fetchObjects.isEmpty { + try fileManager.removeItem(at: fetchObjectsPath) + } else { + let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) + + if fileManager.fileExists(atPath: fetchObjectsPath.path) { + try jsonData.write(to: fetchObjectsPath) + } else { + fileManager.createFile(atPath: fetchObjectsPath.path, contents: jsonData, attributes: nil) + } + } + } + + static fileprivate func saveQueryObjects(_ objects: [T], + queryIdentifier: String) throws { var queryObjects = try getQueryObjects() queryObjects[queryIdentifier] = try objects.map({ try QueryObject($0) }) - let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) + try self.writeQueryObjects(queryObjects) + } + + static fileprivate func getQueryObjects() throws -> [String : [QueryObject]] { + let objectsDirectoryPath = try ParseFileManager.objectsDirectory() + let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) if fileManager.fileExists(atPath: queryObjectsPath.path) { - try jsonData.write(to: queryObjectsPath) + let jsonData = try Data(contentsOf: queryObjectsPath) + do { + return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + } catch { + try fileManager.removeItem(at: queryObjectsPath) + return [:] + } } else { - fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil) + return [:] } } - static func getQueryObjects() throws -> [String : [QueryObject]] { + static private func writeQueryObjects(_ queryObjects: [String : [QueryObject]]) throws { let objectsDirectoryPath = try ParseFileManager.objectsDirectory() let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) - if fileManager.fileExists(atPath: queryObjectsPath.path) { - let jsonData = try Data(contentsOf: queryObjectsPath) - return try ParseCoding.jsonDecoder().decode([String : [QueryObject]].self, from: jsonData) + if queryObjects.isEmpty { + try fileManager.removeItem(at: queryObjectsPath) } else { - return [:] + let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) + + if fileManager.fileExists(atPath: queryObjectsPath.path) { + try jsonData.write(to: queryObjectsPath) + } else { + fileManager.createFile(atPath: queryObjectsPath.path, contents: jsonData, attributes: nil) + } } } @@ -249,6 +291,8 @@ internal struct LocalStorage { case .update: _ = try await objects.updateAll(ignoringLocalStore: true) } + + try self.removeFetchObjects(objects) } } From ca175d06ca6bee6d642cff9192b6a202a082b243 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Mon, 26 Dec 2022 22:33:20 +0100 Subject: [PATCH 50/55] Fixes --- Sources/ParseSwift/Storage/LocalStorage.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 191ff5dff..0545036cc 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -106,9 +106,6 @@ internal struct LocalStorage { static fileprivate func saveFetchObjects(_ objects: [T], method: Method) throws { - let objectsDirectoryPath = try ParseFileManager.objectsDirectory() - let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) - var fetchObjects = try getFetchObjects() fetchObjects.append(contentsOf: try objects.map({ try FetchObject($0, method: method) })) fetchObjects = fetchObjects.uniqueObjectsById @@ -151,7 +148,7 @@ internal struct LocalStorage { let fetchObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.fetchObjectsFile.hiddenFile) if fetchObjects.isEmpty { - try fileManager.removeItem(at: fetchObjectsPath) + try? fileManager.removeItem(at: fetchObjectsPath) } else { let jsonData = try ParseCoding.jsonEncoder().encode(fetchObjects) @@ -193,7 +190,7 @@ internal struct LocalStorage { let queryObjectsPath = objectsDirectoryPath.appendingPathComponent(ParseConstants.queryObjectsFile.hiddenFile) if queryObjects.isEmpty { - try fileManager.removeItem(at: queryObjectsPath) + try? fileManager.removeItem(at: queryObjectsPath) } else { let jsonData = try ParseCoding.jsonEncoder().encode(queryObjects) From 455c78b762f3b8832fae5563029832cdbc7072d4 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:13:03 +0100 Subject: [PATCH 51/55] Fix: don't create when an error --- Sources/ParseSwift/Storage/LocalStorage.swift | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 0545036cc..0be0270c0 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -388,19 +388,25 @@ internal extension Sequence where Element: ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } case .create: if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } else { assertionFailure("Enable custom objectIds") @@ -408,18 +414,24 @@ internal extension Sequence where Element: ParseObject { } case .replace: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } case .update: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects(objects, method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects(objects, method: method) + } + } else { + try LocalStorage.saveAll(objects, queryIdentifier: queryIdentifier) } } } From 65843f797380460f61c1ac8ed7813a99d683ed48 Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:22:00 +0100 Subject: [PATCH 52/55] Fix --- Sources/ParseSwift/Storage/LocalStorage.swift | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 0be0270c0..12a49d5df 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -334,19 +334,25 @@ internal extension ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } case .create: if Parse.configuration.offlinePolicy.canCreate { if Parse.configuration.isRequiringCustomObjectIds { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } else { assertionFailure("Enable custom objectIds") @@ -354,18 +360,24 @@ internal extension ParseObject { } case .replace: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } case .update: if Parse.configuration.offlinePolicy.enabled { - try LocalStorage.save(self, queryIdentifier: queryIdentifier) - - if let error = error, error.hasNoInternetConnection { - try LocalStorage.saveFetchObjects([self], method: method) + if let error = error { + if error.hasNoInternetConnection { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) + try LocalStorage.saveFetchObjects([self], method: method) + } + } else { + try LocalStorage.save(self, queryIdentifier: queryIdentifier) } } } From 25b8ea9811716dacbe68a1441a50de987ccbcf7b Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:31:20 +0100 Subject: [PATCH 53/55] Print --- Sources/ParseSwift/Storage/LocalStorage.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 12a49d5df..10f9c12ac 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -334,6 +334,8 @@ internal extension ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { + print("error") + print(error) if let error = error { if error.hasNoInternetConnection { try LocalStorage.save(self, queryIdentifier: queryIdentifier) From 46c0dd5a4b9410b35427b6b8eef9c42d96938fbf Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:48:25 +0100 Subject: [PATCH 54/55] Fix where save was executed before result --- Sources/ParseSwift/Objects/ParseObject+async.swift | 12 ++++++------ Sources/ParseSwift/Objects/ParseObject.swift | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index 24c3804b2..f8de12f39 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -396,17 +396,17 @@ or disable transactions for this call. case .update: command = try self.updateCommand() } - + let commandResult = try await command + .executeAsync(options: options, + callbackQueue: callbackQueue, + childObjects: savedChildObjects, + childFiles: savedChildFiles) print("2") if !ignoringLocalStore { print("3") try? saveLocally(method: method) } - return try await command - .executeAsync(options: options, - callbackQueue: callbackQueue, - childObjects: savedChildObjects, - childFiles: savedChildFiles) + return commandResult } catch { print("4") let defaultError = ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 8c98d9635..b2e2a8bcd 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1415,16 +1415,16 @@ extension ParseObject { case .update: command = try self.updateCommand() } - - if !ignoringLocalStore { - try? saveLocally(method: method) - } command .executeAsync(options: options, callbackQueue: callbackQueue, childObjects: savedChildObjects, childFiles: savedChildFiles, completion: completion) + + if !ignoringLocalStore { + try? saveLocally(method: method) + } } catch { let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) From a0428e97c944f1111b2097213cda41369254900c Mon Sep 17 00:00:00 2001 From: Damian Van de Kauter Date: Tue, 27 Dec 2022 21:59:18 +0100 Subject: [PATCH 55/55] Cleanup --- Sources/ParseSwift/Objects/ParseObject+async.swift | 5 ----- Sources/ParseSwift/Objects/ParseObject.swift | 1 - Sources/ParseSwift/Storage/LocalStorage.swift | 2 -- 3 files changed, 8 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseObject+async.swift b/Sources/ParseSwift/Objects/ParseObject+async.swift index f8de12f39..3c487f385 100644 --- a/Sources/ParseSwift/Objects/ParseObject+async.swift +++ b/Sources/ParseSwift/Objects/ParseObject+async.swift @@ -383,7 +383,6 @@ or disable transactions for this call. options: API.Options, callbackQueue: DispatchQueue) async throws -> Self { let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options) - print("1") do { let command: API.Command! switch method { @@ -401,20 +400,16 @@ or disable transactions for this call. callbackQueue: callbackQueue, childObjects: savedChildObjects, childFiles: savedChildFiles) - print("2") if !ignoringLocalStore { - print("3") try? saveLocally(method: method) } return commandResult } catch { - print("4") let defaultError = ParseError(code: .unknownError, message: error.localizedDescription) let parseError = error as? ParseError ?? defaultError if !ignoringLocalStore { - print("5") try? saveLocally(method: method, error: parseError) } throw parseError diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index b2e2a8bcd..9a0c32cde 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -1235,7 +1235,6 @@ extension ParseObject { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void ) { - print("01") let method = Method.save #if compiler(>=5.5.2) && canImport(_Concurrency) Task { diff --git a/Sources/ParseSwift/Storage/LocalStorage.swift b/Sources/ParseSwift/Storage/LocalStorage.swift index 10f9c12ac..12a49d5df 100644 --- a/Sources/ParseSwift/Storage/LocalStorage.swift +++ b/Sources/ParseSwift/Storage/LocalStorage.swift @@ -334,8 +334,6 @@ internal extension ParseObject { switch method { case .save: if Parse.configuration.offlinePolicy.enabled { - print("error") - print(error) if let error = error { if error.hasNoInternetConnection { try LocalStorage.save(self, queryIdentifier: queryIdentifier)