Skip to content

Commit

Permalink
fix: async save/create/update/replace use async/await deep save (#418)
Browse files Browse the repository at this point in the history
* decode bad encoded errors from server

* ParseObject uses async/await deep save

* fix: async save/create/update/replace use async/await deep save

* refactor batchCommand

* add deep save async/await tests
  • Loading branch information
cbaker6 authored Sep 17, 2022
1 parent 085f5ad commit 40a7b42
Show file tree
Hide file tree
Showing 15 changed files with 1,707 additions and 62 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.14.0...main)
* _Contributing to this repo? Add info about your change here to be included in the next release_

### 4.14.1
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.14.0...4.14.1)

__Fixes__
- For Swift 5.5.2+ all asynchronous methods that attempt to save, create, update, or replace use the async/await version of deep saving ParseObjects. This fixes any purple warnings caused by the SDK in Xcode. Older Swift versions use the synchronous version of deep saving ([#418](https://github.com/parse-community/Parse-Swift/pull/418)), thanks to [Corey Baker](https://github.com/cbaker6).
- Can catch when the Parse Server throws an improper ParseError that only contains "error" or "message", but does not contain a "code" ([#418](https://github.com/parse-community/Parse-Swift/pull/418)), thanks to [Corey Baker](https://github.com/cbaker6).

### 4.14.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.13.1...4.14.0)

Expand Down
20 changes: 20 additions & 0 deletions ParseSwift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,14 @@
708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
708D035525215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
708EF0BD28D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
708EF0BE28D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
708EF0BF28D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
708EF0C028D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
708EF0C228D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
708EF0C328D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
708EF0C428D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
708EF0C528D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
709A147D283949D100BF85E5 /* ParseSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709A147C283949D100BF85E5 /* ParseSchema.swift */; };
709A147E283949D100BF85E5 /* ParseSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709A147C283949D100BF85E5 /* ParseSchema.swift */; };
709A147F283949D100BF85E5 /* ParseSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709A147C283949D100BF85E5 /* ParseSchema.swift */; };
Expand Down Expand Up @@ -1285,6 +1293,8 @@
7085DDB226D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticationCombineTests.swift; sourceTree = "<group>"; };
708CADCE2872263D0066C279 /* ParseKeychainAccessGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseKeychainAccessGroupTests.swift; sourceTree = "<group>"; };
708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = "<group>"; };
708EF0BC28D5F4140052EF35 /* API+Command+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Command+async.swift"; sourceTree = "<group>"; };
708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+NonParseBodyCommand+async.swift"; sourceTree = "<group>"; };
709A147C283949D100BF85E5 /* ParseSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSchema.swift; sourceTree = "<group>"; };
709A148128395ED100BF85E5 /* ParseSchema+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseSchema+async.swift"; sourceTree = "<group>"; };
709A148628396B1C00BF85E5 /* ParseField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseField.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2202,7 +2212,9 @@
F97B462624D9C72700F4A88B /* API.swift */,
91B79AC726EE3C5D00073F2C /* API+BatchCommand.swift */,
F97B462E24D9C74400F4A88B /* API+Command.swift */,
708EF0BC28D5F4140052EF35 /* API+Command+async.swift */,
91B79AC226EE3A4E00073F2C /* API+NonParseBodyCommand.swift */,
708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */,
F97B462B24D9C74400F4A88B /* BatchUtils.swift */,
7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */,
F97B462D24D9C74400F4A88B /* Responses.swift */,
Expand Down Expand Up @@ -2683,6 +2695,7 @@
916786E2259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
70CE0AC6285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
91F346B9269B766C005727B6 /* CloudViewModel.swift in Sources */,
708EF0C228D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
709A148C2839A1DB00BF85E5 /* Operation.swift in Sources */,
70CE0AD0285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
709A14A5283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
Expand All @@ -2701,6 +2714,7 @@
70170A442656B02D0070C905 /* ParseAnalytics.swift in Sources */,
70110D52250680140091CC1D /* ParseConstants.swift in Sources */,
91B79AC326EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
708EF0BD28D5F4140052EF35 /* API+Command+async.swift in Sources */,
70D1BDBA25BB17A600A42E7C /* ParseConfig.swift in Sources */,
7C4C092B285E746800F202C6 /* ParseInstagram.swift in Sources */,
703B08FD26BD953B005A112F /* ParseHealth+async.swift in Sources */,
Expand Down Expand Up @@ -2995,6 +3009,7 @@
916786E3259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
70CE0AC7285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
91F346BA269B766D005727B6 /* CloudViewModel.swift in Sources */,
708EF0C328D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
709A148D2839A1DB00BF85E5 /* Operation.swift in Sources */,
70CE0AD1285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
709A14A6283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
Expand All @@ -3013,6 +3028,7 @@
70170A452656B02D0070C905 /* ParseAnalytics.swift in Sources */,
70110D53250680140091CC1D /* ParseConstants.swift in Sources */,
91B79AC426EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
708EF0BE28D5F4140052EF35 /* API+Command+async.swift in Sources */,
70D1BDBB25BB17A600A42E7C /* ParseConfig.swift in Sources */,
7C4C092C285E746800F202C6 /* ParseInstagram.swift in Sources */,
703B08FE26BD953B005A112F /* ParseHealth+async.swift in Sources */,
Expand Down Expand Up @@ -3440,6 +3456,7 @@
916786E5259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
70CE0AC9285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
91F346BC269B766D005727B6 /* CloudViewModel.swift in Sources */,
708EF0C528D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
709A148F2839A1DB00BF85E5 /* Operation.swift in Sources */,
70CE0AD3285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
709A14A8283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
Expand All @@ -3458,6 +3475,7 @@
F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */,
70170A472656B02D0070C905 /* ParseAnalytics.swift in Sources */,
F97B45E124D9C6F200F4A88B /* AnyCodable.swift in Sources */,
708EF0C028D5F4140052EF35 /* API+Command+async.swift in Sources */,
91B79AC626EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
7C4C092E285E746800F202C6 /* ParseInstagram.swift in Sources */,
70D1BDBD25BB17A600A42E7C /* ParseConfig.swift in Sources */,
Expand Down Expand Up @@ -3628,6 +3646,7 @@
916786E4259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
70CE0AC8285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
91F346BB269B766D005727B6 /* CloudViewModel.swift in Sources */,
708EF0C428D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
709A148E2839A1DB00BF85E5 /* Operation.swift in Sources */,
70CE0AD2285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
709A14A7283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
Expand All @@ -3646,6 +3665,7 @@
F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */,
70170A462656B02D0070C905 /* ParseAnalytics.swift in Sources */,
F97B45E024D9C6F200F4A88B /* AnyCodable.swift in Sources */,
708EF0BF28D5F4140052EF35 /* API+Command+async.swift in Sources */,
91B79AC526EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
7C4C092D285E746800F202C6 /* ParseInstagram.swift in Sources */,
70D1BDBC25BB17A600A42E7C /* ParseConfig.swift in Sources */,
Expand Down
37 changes: 37 additions & 0 deletions Sources/ParseSwift/API/API+Command+async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// API+Command+async.swift
// ParseSwift
//
// Created by Corey Baker on 9/17/22.
// Copyright © 2022 Parse Community. All rights reserved.
//

#if compiler(>=5.5.2) && canImport(_Concurrency)
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

internal extension API.Command {
// MARK: Asynchronous Execution
func executeAsync(options: API.Options,
callbackQueue: DispatchQueue,
notificationQueue: DispatchQueue? = nil,
childObjects: [String: PointerType]? = nil,
childFiles: [UUID: ParseFile]? = nil,
uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
// swiftlint:disable:next line_length
downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil) async throws -> U {
try await withCheckedThrowingContinuation { continuation in
self.executeAsync(options: options,
callbackQueue: callbackQueue,
notificationQueue: notificationQueue,
childObjects: childObjects,
childFiles: childFiles,
uploadProgress: uploadProgress,
downloadProgress: downloadProgress,
completion: continuation.resume)
}
}
}
#endif
26 changes: 26 additions & 0 deletions Sources/ParseSwift/API/API+NonParseBodyCommand+async.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// API+NonParseBodyCommand+async.swift
// ParseSwift
//
// Created by Corey Baker on 9/17/22.
// Copyright © 2022 Parse Community. All rights reserved.
//

#if compiler(>=5.5.2) && canImport(_Concurrency)
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

extension API.NonParseBodyCommand {
// MARK: Asynchronous Execution
func executeAsync(options: API.Options,
callbackQueue: DispatchQueue) async throws -> U {
try await withCheckedThrowingContinuation { continuation in
self.executeAsync(options: options,
callbackQueue: callbackQueue,
completion: continuation.resume)
}
}
}
#endif
112 changes: 112 additions & 0 deletions Sources/ParseSwift/Objects/ParseInstallation+async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,118 @@ public extension Sequence where Element: ParseInstallation {
}
}

// MARK: Helper Methods (Internal)
internal extension ParseInstallation {

func command(method: Method,
ignoringCustomObjectIdConfig: Bool = false,
options: API.Options,
callbackQueue: DispatchQueue) async throws -> Self {
let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options)
do {
let command: API.Command<Self, Self>!
switch method {
case .save:
command = try self.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
case .create:
command = self.createCommand()
case .replace:
command = try self.replaceCommand()
case .update:
command = try self.updateCommand()
}
let saved = try await command
.executeAsync(options: options,
callbackQueue: callbackQueue,
childObjects: savedChildObjects,
childFiles: savedChildFiles)
try? Self.updateKeychainIfNeeded([saved])
return saved
} catch {
let defaultError = ParseError(code: .unknownError,
message: error.localizedDescription)
let parseError = error as? ParseError ?? defaultError
throw parseError
}
}
}

// MARK: Batch Support
internal extension Sequence where Element: ParseInstallation {
func batchCommand(method: Method,
batchLimit limit: Int?,
transaction: Bool,
ignoringCustomObjectIdConfig: Bool = false,
options: API.Options,
callbackQueue: DispatchQueue) async throws -> [(Result<Element, ParseError>)] {
var options = options
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
var childObjects = [String: PointerType]()
var childFiles = [UUID: ParseFile]()
var commands = [API.Command<Self.Element, Self.Element>]()
let objects = map { $0 }
for object in objects {
let (savedChildObjects, savedChildFiles) = try await object
.ensureDeepSave(options: options,
isShouldReturnIfChildObjectsFound: transaction)
try savedChildObjects.forEach {(key, value) in
guard childObjects[key] == nil else {
throw ParseError(code: .unknownError, message: "circular dependency")
}
childObjects[key] = value
}
try savedChildFiles.forEach {(key, value) in
guard childFiles[key] == nil else {
throw ParseError(code: .unknownError, message: "circular dependency")
}
childFiles[key] = value
}
do {
switch method {
case .save:
commands.append(
try object.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
)
case .create:
commands.append(object.createCommand())
case .replace:
commands.append(try object.replaceCommand())
case .update:
commands.append(try object.updateCommand())
}
} catch {
let defaultError = ParseError(code: .unknownError,
message: error.localizedDescription)
let parseError = error as? ParseError ?? defaultError
throw parseError
}
}

do {
var returnBatch = [(Result<Self.Element, ParseError>)]()
let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit
try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit)
let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit)
for batch in batches {
let saved = try await API.Command<Self.Element, Self.Element>
.batch(commands: batch, transaction: transaction)
.executeAsync(options: options,
callbackQueue: callbackQueue,
childObjects: childObjects,
childFiles: childFiles)
returnBatch.append(contentsOf: saved)
}
try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()})
return returnBatch
} catch {
let defaultError = ParseError(code: .unknownError,
message: error.localizedDescription)
let parseError = error as? ParseError ?? defaultError
throw parseError
}
}
}

#if !os(Linux) && !os(Android) && !os(Windows)
// MARK: Migrate from Objective-C SDK
public extension ParseInstallation {
Expand Down
Loading

0 comments on commit 40a7b42

Please sign in to comment.