Skip to content

Commit 40a7b42

Browse files
authored
fix: async save/create/update/replace use async/await deep save (#418)
* 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
1 parent 085f5ad commit 40a7b42

15 files changed

+1707
-62
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.14.0...main)
55
* _Contributing to this repo? Add info about your change here to be included in the next release_
66

7+
### 4.14.1
8+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.14.0...4.14.1)
9+
10+
__Fixes__
11+
- 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).
12+
- 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).
13+
714
### 4.14.0
815
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.13.1...4.14.0)
916

ParseSwift.xcodeproj/project.pbxproj

+20
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,14 @@
469469
708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
470470
708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
471471
708D035525215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; };
472+
708EF0BD28D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
473+
708EF0BE28D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
474+
708EF0BF28D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
475+
708EF0C028D5F4140052EF35 /* API+Command+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0BC28D5F4140052EF35 /* API+Command+async.swift */; };
476+
708EF0C228D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
477+
708EF0C328D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
478+
708EF0C428D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
479+
708EF0C528D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */; };
472480
709A147D283949D100BF85E5 /* ParseSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709A147C283949D100BF85E5 /* ParseSchema.swift */; };
473481
709A147E283949D100BF85E5 /* ParseSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709A147C283949D100BF85E5 /* ParseSchema.swift */; };
474482
709A147F283949D100BF85E5 /* ParseSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 709A147C283949D100BF85E5 /* ParseSchema.swift */; };
@@ -1285,6 +1293,8 @@
12851293
7085DDB226D1EC7F0033B977 /* ParseAuthenticationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticationCombineTests.swift; sourceTree = "<group>"; };
12861294
708CADCE2872263D0066C279 /* ParseKeychainAccessGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseKeychainAccessGroupTests.swift; sourceTree = "<group>"; };
12871295
708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = "<group>"; };
1296+
708EF0BC28D5F4140052EF35 /* API+Command+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Command+async.swift"; sourceTree = "<group>"; };
1297+
708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+NonParseBodyCommand+async.swift"; sourceTree = "<group>"; };
12881298
709A147C283949D100BF85E5 /* ParseSchema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSchema.swift; sourceTree = "<group>"; };
12891299
709A148128395ED100BF85E5 /* ParseSchema+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseSchema+async.swift"; sourceTree = "<group>"; };
12901300
709A148628396B1C00BF85E5 /* ParseField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseField.swift; sourceTree = "<group>"; };
@@ -2202,7 +2212,9 @@
22022212
F97B462624D9C72700F4A88B /* API.swift */,
22032213
91B79AC726EE3C5D00073F2C /* API+BatchCommand.swift */,
22042214
F97B462E24D9C74400F4A88B /* API+Command.swift */,
2215+
708EF0BC28D5F4140052EF35 /* API+Command+async.swift */,
22052216
91B79AC226EE3A4E00073F2C /* API+NonParseBodyCommand.swift */,
2217+
708EF0C128D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift */,
22062218
F97B462B24D9C74400F4A88B /* BatchUtils.swift */,
22072219
7003972925A3B0130052CB31 /* ParseURLSessionDelegate.swift */,
22082220
F97B462D24D9C74400F4A88B /* Responses.swift */,
@@ -2683,6 +2695,7 @@
26832695
916786E2259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
26842696
70CE0AC6285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
26852697
91F346B9269B766C005727B6 /* CloudViewModel.swift in Sources */,
2698+
708EF0C228D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
26862699
709A148C2839A1DB00BF85E5 /* Operation.swift in Sources */,
26872700
70CE0AD0285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
26882701
709A14A5283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
@@ -2701,6 +2714,7 @@
27012714
70170A442656B02D0070C905 /* ParseAnalytics.swift in Sources */,
27022715
70110D52250680140091CC1D /* ParseConstants.swift in Sources */,
27032716
91B79AC326EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
2717+
708EF0BD28D5F4140052EF35 /* API+Command+async.swift in Sources */,
27042718
70D1BDBA25BB17A600A42E7C /* ParseConfig.swift in Sources */,
27052719
7C4C092B285E746800F202C6 /* ParseInstagram.swift in Sources */,
27062720
703B08FD26BD953B005A112F /* ParseHealth+async.swift in Sources */,
@@ -2995,6 +3009,7 @@
29953009
916786E3259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
29963010
70CE0AC7285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
29973011
91F346BA269B766D005727B6 /* CloudViewModel.swift in Sources */,
3012+
708EF0C328D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
29983013
709A148D2839A1DB00BF85E5 /* Operation.swift in Sources */,
29993014
70CE0AD1285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
30003015
709A14A6283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
@@ -3013,6 +3028,7 @@
30133028
70170A452656B02D0070C905 /* ParseAnalytics.swift in Sources */,
30143029
70110D53250680140091CC1D /* ParseConstants.swift in Sources */,
30153030
91B79AC426EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
3031+
708EF0BE28D5F4140052EF35 /* API+Command+async.swift in Sources */,
30163032
70D1BDBB25BB17A600A42E7C /* ParseConfig.swift in Sources */,
30173033
7C4C092C285E746800F202C6 /* ParseInstagram.swift in Sources */,
30183034
703B08FE26BD953B005A112F /* ParseHealth+async.swift in Sources */,
@@ -3440,6 +3456,7 @@
34403456
916786E5259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
34413457
70CE0AC9285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
34423458
91F346BC269B766D005727B6 /* CloudViewModel.swift in Sources */,
3459+
708EF0C528D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
34433460
709A148F2839A1DB00BF85E5 /* Operation.swift in Sources */,
34443461
70CE0AD3285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
34453462
709A14A8283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
@@ -3458,6 +3475,7 @@
34583475
F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */,
34593476
70170A472656B02D0070C905 /* ParseAnalytics.swift in Sources */,
34603477
F97B45E124D9C6F200F4A88B /* AnyCodable.swift in Sources */,
3478+
708EF0C028D5F4140052EF35 /* API+Command+async.swift in Sources */,
34613479
91B79AC626EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
34623480
7C4C092E285E746800F202C6 /* ParseInstagram.swift in Sources */,
34633481
70D1BDBD25BB17A600A42E7C /* ParseConfig.swift in Sources */,
@@ -3628,6 +3646,7 @@
36283646
916786E4259B7DDA00BB5B4E /* ParseCloudable.swift in Sources */,
36293647
70CE0AC8285FD5A800DAEA86 /* ParseHookFunctionable+combine.swift in Sources */,
36303648
91F346BB269B766D005727B6 /* CloudViewModel.swift in Sources */,
3649+
708EF0C428D5FDF10052EF35 /* API+NonParseBodyCommand+async.swift in Sources */,
36313650
709A148E2839A1DB00BF85E5 /* Operation.swift in Sources */,
36323651
70CE0AD2285FD5D700DAEA86 /* ParseHookTriggerable+combine.swift in Sources */,
36333652
709A14A7283AAF4C00BF85E5 /* ParseSchema+combine.swift in Sources */,
@@ -3646,6 +3665,7 @@
36463665
F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */,
36473666
70170A462656B02D0070C905 /* ParseAnalytics.swift in Sources */,
36483667
F97B45E024D9C6F200F4A88B /* AnyCodable.swift in Sources */,
3668+
708EF0BF28D5F4140052EF35 /* API+Command+async.swift in Sources */,
36493669
91B79AC526EE3A4E00073F2C /* API+NonParseBodyCommand.swift in Sources */,
36503670
7C4C092D285E746800F202C6 /* ParseInstagram.swift in Sources */,
36513671
70D1BDBC25BB17A600A42E7C /* ParseConfig.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// API+Command+async.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 9/17/22.
6+
// Copyright © 2022 Parse Community. All rights reserved.
7+
//
8+
9+
#if compiler(>=5.5.2) && canImport(_Concurrency)
10+
import Foundation
11+
#if canImport(FoundationNetworking)
12+
import FoundationNetworking
13+
#endif
14+
15+
internal extension API.Command {
16+
// MARK: Asynchronous Execution
17+
func executeAsync(options: API.Options,
18+
callbackQueue: DispatchQueue,
19+
notificationQueue: DispatchQueue? = nil,
20+
childObjects: [String: PointerType]? = nil,
21+
childFiles: [UUID: ParseFile]? = nil,
22+
uploadProgress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil,
23+
// swiftlint:disable:next line_length
24+
downloadProgress: ((URLSessionDownloadTask, Int64, Int64, Int64) -> Void)? = nil) async throws -> U {
25+
try await withCheckedThrowingContinuation { continuation in
26+
self.executeAsync(options: options,
27+
callbackQueue: callbackQueue,
28+
notificationQueue: notificationQueue,
29+
childObjects: childObjects,
30+
childFiles: childFiles,
31+
uploadProgress: uploadProgress,
32+
downloadProgress: downloadProgress,
33+
completion: continuation.resume)
34+
}
35+
}
36+
}
37+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// API+NonParseBodyCommand+async.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 9/17/22.
6+
// Copyright © 2022 Parse Community. All rights reserved.
7+
//
8+
9+
#if compiler(>=5.5.2) && canImport(_Concurrency)
10+
import Foundation
11+
#if canImport(FoundationNetworking)
12+
import FoundationNetworking
13+
#endif
14+
15+
extension API.NonParseBodyCommand {
16+
// MARK: Asynchronous Execution
17+
func executeAsync(options: API.Options,
18+
callbackQueue: DispatchQueue) async throws -> U {
19+
try await withCheckedThrowingContinuation { continuation in
20+
self.executeAsync(options: options,
21+
callbackQueue: callbackQueue,
22+
completion: continuation.resume)
23+
}
24+
}
25+
}
26+
#endif

Sources/ParseSwift/Objects/ParseInstallation+async.swift

+112
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,118 @@ public extension Sequence where Element: ParseInstallation {
328328
}
329329
}
330330

331+
// MARK: Helper Methods (Internal)
332+
internal extension ParseInstallation {
333+
334+
func command(method: Method,
335+
ignoringCustomObjectIdConfig: Bool = false,
336+
options: API.Options,
337+
callbackQueue: DispatchQueue) async throws -> Self {
338+
let (savedChildObjects, savedChildFiles) = try await self.ensureDeepSave(options: options)
339+
do {
340+
let command: API.Command<Self, Self>!
341+
switch method {
342+
case .save:
343+
command = try self.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
344+
case .create:
345+
command = self.createCommand()
346+
case .replace:
347+
command = try self.replaceCommand()
348+
case .update:
349+
command = try self.updateCommand()
350+
}
351+
let saved = try await command
352+
.executeAsync(options: options,
353+
callbackQueue: callbackQueue,
354+
childObjects: savedChildObjects,
355+
childFiles: savedChildFiles)
356+
try? Self.updateKeychainIfNeeded([saved])
357+
return saved
358+
} catch {
359+
let defaultError = ParseError(code: .unknownError,
360+
message: error.localizedDescription)
361+
let parseError = error as? ParseError ?? defaultError
362+
throw parseError
363+
}
364+
}
365+
}
366+
367+
// MARK: Batch Support
368+
internal extension Sequence where Element: ParseInstallation {
369+
func batchCommand(method: Method,
370+
batchLimit limit: Int?,
371+
transaction: Bool,
372+
ignoringCustomObjectIdConfig: Bool = false,
373+
options: API.Options,
374+
callbackQueue: DispatchQueue) async throws -> [(Result<Element, ParseError>)] {
375+
var options = options
376+
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
377+
var childObjects = [String: PointerType]()
378+
var childFiles = [UUID: ParseFile]()
379+
var commands = [API.Command<Self.Element, Self.Element>]()
380+
let objects = map { $0 }
381+
for object in objects {
382+
let (savedChildObjects, savedChildFiles) = try await object
383+
.ensureDeepSave(options: options,
384+
isShouldReturnIfChildObjectsFound: transaction)
385+
try savedChildObjects.forEach {(key, value) in
386+
guard childObjects[key] == nil else {
387+
throw ParseError(code: .unknownError, message: "circular dependency")
388+
}
389+
childObjects[key] = value
390+
}
391+
try savedChildFiles.forEach {(key, value) in
392+
guard childFiles[key] == nil else {
393+
throw ParseError(code: .unknownError, message: "circular dependency")
394+
}
395+
childFiles[key] = value
396+
}
397+
do {
398+
switch method {
399+
case .save:
400+
commands.append(
401+
try object.saveCommand(ignoringCustomObjectIdConfig: ignoringCustomObjectIdConfig)
402+
)
403+
case .create:
404+
commands.append(object.createCommand())
405+
case .replace:
406+
commands.append(try object.replaceCommand())
407+
case .update:
408+
commands.append(try object.updateCommand())
409+
}
410+
} catch {
411+
let defaultError = ParseError(code: .unknownError,
412+
message: error.localizedDescription)
413+
let parseError = error as? ParseError ?? defaultError
414+
throw parseError
415+
}
416+
}
417+
418+
do {
419+
var returnBatch = [(Result<Self.Element, ParseError>)]()
420+
let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit
421+
try canSendTransactions(transaction, objectCount: commands.count, batchLimit: batchLimit)
422+
let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit)
423+
for batch in batches {
424+
let saved = try await API.Command<Self.Element, Self.Element>
425+
.batch(commands: batch, transaction: transaction)
426+
.executeAsync(options: options,
427+
callbackQueue: callbackQueue,
428+
childObjects: childObjects,
429+
childFiles: childFiles)
430+
returnBatch.append(contentsOf: saved)
431+
}
432+
try? Self.Element.updateKeychainIfNeeded(returnBatch.compactMap {try? $0.get()})
433+
return returnBatch
434+
} catch {
435+
let defaultError = ParseError(code: .unknownError,
436+
message: error.localizedDescription)
437+
let parseError = error as? ParseError ?? defaultError
438+
throw parseError
439+
}
440+
}
441+
}
442+
331443
#if !os(Linux) && !os(Android) && !os(Windows)
332444
// MARK: Migrate from Objective-C SDK
333445
public extension ParseInstallation {

0 commit comments

Comments
 (0)