Skip to content

Commit ed55608

Browse files
authored
fix: Storage access should wait until SDK initializes (#65)
* fix: Storage access should wait until SDK initializes * fix tear down * nit * fix app crash * Force SDK calls to wait until init finished * increase codecov
1 parent 16c9ec6 commit ed55608

14 files changed

+255
-90
lines changed

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22
# Parse-Swift Changelog
33

44
### main
5-
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.0.0...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
5+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.0.1...main), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/main/documentation/parseswift)
66
* _Contributing to this repo? Add info about your change here to be included in the next release_
77

8+
### 5.0.1
9+
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.0.0...5.0.1), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.0.1/documentation/parseswift)
10+
11+
__Fixes__
12+
* Access to all ParseStorage (.current() objects) yields until SDK has completed initialization ([#63](https://github.com/netreconlab/Parse-Swift/pull/63)), thanks to [Corey Baker](https://github.com/cbaker6).
13+
814
### 5.0.0
915
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/4.16.2...5.0.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.0.0/documentation/parseswift)
1016

1117
__Breaking Changes__
1218
* Current objects such as ParseObject, ParseUser, ParseVersion, etc. now require try async/await. All synchronous networking and local storage calls have been removed. Please look at the updated Swift Playgrounds for examples ([#62](https://github.com/netreconlab/Parse-Swift/pull/62)), thanks to [Corey Baker](https://github.com/cbaker6).
1319
* ParseHookTriggerRequest has been renamed to ParseHookTriggerObjectRequest as it is used for decoding triggers related to ParseObjects. The new ParseHookTriggerRequest is similar but used for decoding requests not related to ParseObjects like ParseFile ([#53](https://github.com/netreconlab/Parse-Swift/pull/53)), thanks to [Corey Baker](https://github.com/cbaker6).
14-
* ParseVersion now supports pre-release versions of the SDK ([#49](https://github.com/netreconlab/Parse-Swift/pull/49)), thanks to [Corey Baker](https://github.com/cbaker6).
1520
* Added a new ParseHealth.Status enum to support new feature in Parse Server 6.0.0.
1621
Developers can now receive intermediate status updates (Status.initialized, Status.starting)
1722
using the ParseHealth.check callback or Combine methods. Status.initialized and
@@ -39,7 +44,7 @@ __New features__
3944
* The max connection attempts for LiveQuery can now be changed when initializing the SDK ([#43](https://github.com/netreconlab/Parse-Swift/pull/43)), thanks to [Corey Baker](https://github.com/cbaker6).
4045

4146
__Fixes__
42-
* Fixed "Duplicate request" error when resending requests related to ipempotency ([#63](https://github.com/netreconlab/Parse-Swift/pull/63)), thanks to [Corey Baker](https://github.com/cbaker6).
47+
* Fixed "Duplicate request" error when resending requests related to idempotency ([#63](https://github.com/netreconlab/Parse-Swift/pull/63)), thanks to [Corey Baker](https://github.com/cbaker6).
4348
* Fixed query count and withCount returning 0 when the SDK is configured to use GET for queries ([#61](https://github.com/netreconlab/Parse-Swift/pull/61)), thanks to [Corey Baker](https://github.com/cbaker6).
4449
* Fixed ambiguous ParseAnalytics trackAppOpenned ([#55](https://github.com/netreconlab/Parse-Swift/pull/55)), thanks to [Corey Baker](https://github.com/cbaker6).
4550
* Refactored playground mount to be "/parse" instead "/1". Also do not require url when decoding a ParseFile ([#52](https://github.com/netreconlab/Parse-Swift/pull/52)), thanks to [Corey Baker](https://github.com/cbaker6).

Sources/ParseSwift/API/API.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ public struct API {
214214
headers["X-Parse-Session-Token"] = token
215215
}
216216

217-
if let installationId = try? await BaseParseInstallation.current().installationId {
217+
if let installationId = await BaseParseInstallation.currentContainer().installationId {
218218
headers["X-Parse-Installation-Id"] = installationId
219219
}
220220

Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import FoundationNetworking
2020
```swift
2121
// If "Message" is a "ParseObject"
2222
let myQuery = Message.query("from" == "parse")
23-
guard let subscription = myQuery.subscribe else {
23+
guard let subscription = try await myQuery.subscribe() else {
2424
"Error subscribing..."
2525
return
2626
}
@@ -93,9 +93,14 @@ public final class ParseLiveQuery: NSObject {
9393
}
9494
}
9595

96+
/// Current LiveQuery client.
97+
public private(set) static var client: ParseLiveQuery?
98+
9699
/// The current status of the LiveQuery socket.
97100
public internal(set) var status: ConnectionStatus = .socketNotEstablished
98101

102+
static var isConfiguring: Bool = false
103+
99104
let notificationQueue: DispatchQueue
100105
var task: URLSessionWebSocketTask!
101106
var url: URL!
@@ -155,6 +160,7 @@ Not attempting to open ParseLiveQuery socket anymore
155160
try await self.resumeTask()
156161
if isDefault {
157162
Self.defaultClient = self
163+
Self.isConfiguring = false
158164
}
159165
}
160166

@@ -165,6 +171,38 @@ Not attempting to open ParseLiveQuery socket anymore
165171
}
166172
authenticationDelegate = nil
167173
receiveDelegate = nil
174+
if Self.client == self {
175+
Self.isConfiguring = false
176+
}
177+
}
178+
179+
static func yieldIfNotConfigured() async {
180+
guard !isConfiguring else {
181+
await Task.yield()
182+
await yieldIfNotConfigured()
183+
return
184+
}
185+
}
186+
187+
static func configure() async throws {
188+
guard Self.client == nil else {
189+
return
190+
}
191+
guard !isConfiguring else {
192+
await yieldIfNotConfigured()
193+
return
194+
}
195+
isConfiguring = true
196+
await yieldIfNotInitialized()
197+
Self.defaultClient = try await Self(isDefault: true)
198+
}
199+
200+
static func client() async throws -> ParseLiveQuery {
201+
try await configure()
202+
guard let client = Self.client else {
203+
throw ParseError(code: .otherCause, message: "Missing LiveQuery client")
204+
}
205+
return client
168206
}
169207

170208
func setStatus(_ status: ConnectionStatus) async {
@@ -222,9 +260,6 @@ Not attempting to open ParseLiveQuery socket anymore
222260
// MARK: Client Intents
223261
extension ParseLiveQuery {
224262

225-
/// Current LiveQuery client.
226-
public private(set) static var client: ParseLiveQuery?
227-
228263
func resumeTask() async throws {
229264
switch self.task.state {
230265
case .suspended:
@@ -835,10 +870,7 @@ public extension Query {
835870
- throws: An error of type `ParseError`.
836871
*/
837872
func subscribe() async throws -> Subscription<ResultType> {
838-
guard let client = ParseLiveQuery.client else {
839-
throw ParseError(code: .otherCause, message: "Missing LiveQuery client")
840-
}
841-
return try await client.subscribe(self)
873+
try await ParseLiveQuery.client().subscribe(self)
842874
}
843875

844876
/**
@@ -862,11 +894,7 @@ public extension Query {
862894
- throws: An error of type `ParseError`.
863895
*/
864896
static func subscribe<T: QuerySubscribable>(_ handler: T) async throws -> T {
865-
if let client = ParseLiveQuery.client {
866-
return try await client.subscribe(handler)
867-
} else {
868-
throw ParseError(code: .otherCause, message: "ParseLiveQuery Error: Not able to initialize client.")
869-
}
897+
try await ParseLiveQuery.client().subscribe(handler)
870898
}
871899

872900
/**
@@ -877,7 +905,7 @@ public extension Query {
877905
- throws: An error of type `ParseError`.
878906
*/
879907
static func subscribe<T: QuerySubscribable>(_ handler: T, client: ParseLiveQuery) async throws -> T {
880-
try await client.subscribe(handler)
908+
try await ParseLiveQuery.client().subscribe(handler)
881909
}
882910

883911
/**
@@ -887,10 +915,7 @@ public extension Query {
887915
- throws: An error of type `ParseError`.
888916
*/
889917
func subscribeCallback() async throws -> SubscriptionCallback<ResultType> {
890-
guard let client = ParseLiveQuery.client else {
891-
throw ParseError(code: .otherCause, message: "Missing LiveQuery client")
892-
}
893-
return try await client.subscribe(SubscriptionCallback(query: self))
918+
try await ParseLiveQuery.client().subscribe(SubscriptionCallback(query: self))
894919
}
895920

896921
/**
@@ -913,7 +938,7 @@ public extension Query {
913938
- throws: An error of type `ParseError`.
914939
*/
915940
func unsubscribe() async throws {
916-
try await ParseLiveQuery.client?.unsubscribe(self)
941+
try await ParseLiveQuery.client().unsubscribe(self)
917942
}
918943

919944
/**
@@ -933,7 +958,7 @@ public extension Query {
933958
- throws: An error of type `ParseError`.
934959
*/
935960
func unsubscribe<T: QuerySubscribable>(_ handler: T) async throws {
936-
try await ParseLiveQuery.client?.unsubscribe(handler)
961+
try await ParseLiveQuery.client().unsubscribe(handler)
937962
}
938963

939964
/**
@@ -957,7 +982,7 @@ public extension Query {
957982
- throws: An error of type `ParseError`.
958983
*/
959984
func update<T: QuerySubscribable>(_ handler: T) async throws {
960-
try await ParseLiveQuery.client?.update(handler)
985+
try await ParseLiveQuery.client().update(handler)
961986
}
962987

963988
/**

Sources/ParseSwift/Objects/ParseInstallation.swift

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -204,23 +204,32 @@ struct CurrentInstallationContainer<T: ParseInstallation>: Codable, Hashable {
204204

205205
// MARK: Current Installation Support
206206
public extension ParseInstallation {
207+
208+
internal static func create() async throws {
209+
let newInstallationId = UUID().uuidString.lowercased()
210+
var newInstallation = BaseParseInstallation()
211+
newInstallation.installationId = newInstallationId
212+
newInstallation.createInstallationId(newId: newInstallationId)
213+
newInstallation.updateAutomaticInfo()
214+
let newBaseInstallationContainer =
215+
CurrentInstallationContainer<BaseParseInstallation>(currentInstallation: newInstallation,
216+
installationId: newInstallationId)
217+
try await ParseStorage.shared.set(newBaseInstallationContainer,
218+
for: ParseStorage.Keys.currentInstallation)
219+
#if !os(Linux) && !os(Android) && !os(Windows)
220+
try? await KeychainStore.shared.set(newBaseInstallationContainer,
221+
for: ParseStorage.Keys.currentInstallation)
222+
#endif
223+
}
224+
207225
internal static func currentContainer() async -> CurrentInstallationContainer<Self> {
208226
guard let installationInMemory: CurrentInstallationContainer<Self> =
209227
try? await ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else {
210228
#if !os(Linux) && !os(Android) && !os(Windows)
211229
guard let installationFromKeyChain: CurrentInstallationContainer<Self> =
212230
try? await KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation)
213231
else {
214-
let newInstallationId = UUID().uuidString.lowercased()
215-
var newInstallation = BaseParseInstallation()
216-
newInstallation.installationId = newInstallationId
217-
newInstallation.createInstallationId(newId: newInstallationId)
218-
newInstallation.updateAutomaticInfo()
219-
let newBaseInstallationContainer =
220-
CurrentInstallationContainer<BaseParseInstallation>(currentInstallation: newInstallation,
221-
installationId: newInstallationId)
222-
try? await KeychainStore.shared.set(newBaseInstallationContainer,
223-
for: ParseStorage.Keys.currentInstallation)
232+
try? await create()
224233
guard let installationFromKeyChain: CurrentInstallationContainer<Self> =
225234
try? await KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation)
226235
else {
@@ -232,16 +241,7 @@ public extension ParseInstallation {
232241
}
233242
return installationFromKeyChain
234243
#else
235-
let newInstallationId = UUID().uuidString.lowercased()
236-
var newInstallation = BaseParseInstallation()
237-
newInstallation.installationId = newInstallationId
238-
newInstallation.createInstallationId(newId: newInstallationId)
239-
newInstallation.updateAutomaticInfo()
240-
let newBaseInstallationContainer =
241-
CurrentInstallationContainer<BaseParseInstallation>(currentInstallation: newInstallation,
242-
installationId: newInstallationId)
243-
try? await ParseStorage.shared.set(newBaseInstallationContainer,
244-
for: ParseStorage.Keys.currentInstallation)
244+
try? await create()
245245
guard let installationFromMemory: CurrentInstallationContainer<Self> =
246246
try? await ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation)
247247
else {
@@ -286,6 +286,7 @@ public extension ParseInstallation {
286286
- throws: An error of `ParseError` type.
287287
*/
288288
static func current() async throws -> Self {
289+
await yieldIfNotInitialized()
289290
guard let installation = await Self.currentContainer().currentInstallation else {
290291
throw ParseError(code: .otherCause,
291292
message: "There is no current Installation")

Sources/ParseSwift/Objects/ParseUser.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ public extension ParseUser {
163163
- throws: An error of `ParseError` type.
164164
*/
165165
static func current() async throws -> Self {
166+
await yieldIfNotInitialized()
166167
guard let container = await Self.currentContainer(),
167168
let user = container.currentUser else {
168169
throw ParseError(code: .otherCause,

Sources/ParseSwift/Parse.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,20 @@ internal func initialize(applicationId: String,
6161
try await initialize(configuration: configuration)
6262
}
6363

64+
internal func yieldIfNotInitialized() async {
65+
guard ParseConfiguration.checkIfConfigured() else {
66+
await Task.yield()
67+
await yieldIfNotInitialized()
68+
return
69+
}
70+
}
71+
6472
internal func deleteKeychainIfNeeded() async {
6573
#if !os(Linux) && !os(Android) && !os(Windows)
6674
// Clear items out of the Keychain on app first run.
6775
if UserDefaults.standard.object(forKey: ParseConstants.bundlePrefix) == nil {
6876
if Parse.configuration.isDeletingKeychainIfNeeded {
69-
try? await KeychainStore.old.deleteAll()
77+
try? await KeychainStore.old?.deleteAll()
7078
try? await KeychainStore.shared.deleteAll()
7179
}
7280
Parse.configuration.keychainAccessGroup = .init()
@@ -137,7 +145,7 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif
137145
}
138146
} catch {
139147
// Migrate old installations made with ParseSwift < 1.3.0
140-
if let currentInstallation = try? await BaseParseInstallation.current() {
148+
if let currentInstallation = await BaseParseInstallation.currentContainer().currentInstallation {
141149
if currentInstallation.objectId == nil {
142150
await BaseParseInstallation.deleteCurrentContainerFromKeychain()
143151
// Prepare installation
@@ -166,23 +174,22 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif
166174
await BaseParseInstallation.createNewInstallationIfNeeded()
167175

168176
#if !os(Linux) && !os(Android) && !os(Windows)
169-
ParseLiveQuery.defaultClient = try await ParseLiveQuery(isDefault: true)
170177
if configuration.isMigratingFromObjcSDK {
171178
await KeychainStore.createObjectiveC()
172179
if let objcParseKeychain = KeychainStore.objectiveC {
173180
guard let installationId: String = await objcParseKeychain.objectObjectiveC(forKey: "installationId"),
174-
try await BaseParseInstallation.current().installationId != installationId else {
181+
currentInstallationContainer.installationId != installationId else {
182+
Parse.configuration.isInitialized = true
175183
return
176184
}
177-
var updatedInstallation = try await BaseParseInstallation.current()
178-
updatedInstallation.installationId = installationId
179185
var currentInstallationContainer = await BaseParseInstallation.currentContainer()
180186
currentInstallationContainer.installationId = installationId
181-
currentInstallationContainer.currentInstallation = updatedInstallation
187+
currentInstallationContainer.currentInstallation?.installationId = installationId
182188
await BaseParseInstallation.setCurrentContainer(currentInstallationContainer)
183189
}
184190
}
185191
#endif
192+
Parse.configuration.isInitialized = true
186193
}
187194

188195
/**
@@ -340,6 +347,7 @@ public func deleteObjectiveCKeychain() async throws {
340347
throw ParseError(code: .otherCause,
341348
message: "\"accessGroup\" must be set to a valid string when \"synchronizeAcrossDevices == true\"")
342349
}
350+
await yieldIfNotInitialized()
343351
guard let currentAccessGroup = try? await ParseKeychainAccessGroup.current() else {
344352
throw ParseError(code: .otherCause,
345353
message: "Problem unwrapping the current access group. Did you initialize the SDK before calling this method?")

Sources/ParseSwift/ParseConstants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum ParseConstants {
1212
static let sdk = "swift"
13-
static let version = "5.0.0"
13+
static let version = "5.0.1"
1414
static let fileManagementDirectory = "parse/"
1515
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
1616
static let fileManagementLibraryDirectory = "Library/"

Sources/ParseSwift/Storage/ParseStorage.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
// MARK: ParseStorage
99
actor ParseStorage {
10-
public static var shared = ParseStorage()
10+
static var shared = ParseStorage()
1111

12-
private var backingStore: ParsePrimitiveStorable!
12+
var backingStore: ParsePrimitiveStorable!
1313

1414
func use(_ store: ParsePrimitiveStorable) {
1515
self.backingStore = store
@@ -33,27 +33,31 @@ actor ParseStorage {
3333
static let currentVersion = "_currentVersion"
3434
static let currentAccessGroup = "_currentAccessGroup"
3535
}
36+
37+
func setBackingStoreToNil() {
38+
backingStore = nil
39+
}
3640
}
3741

3842
// MARK: Act as a proxy for ParsePrimitiveStorable
3943
extension ParseStorage {
4044

41-
public func delete(valueFor key: String) async throws {
45+
func delete(valueFor key: String) async throws {
4246
try requireBackingStore()
4347
return try await backingStore.delete(valueFor: key)
4448
}
4549

46-
public func deleteAll() async throws {
50+
func deleteAll() async throws {
4751
try requireBackingStore()
4852
return try await backingStore.deleteAll()
4953
}
5054

51-
public func get<T>(valueFor key: String) async throws -> T? where T: Decodable {
55+
func get<T>(valueFor key: String) async throws -> T? where T: Decodable {
5256
try requireBackingStore()
5357
return try await backingStore.get(valueFor: key)
5458
}
5559

56-
public func set<T>(_ object: T, for key: String) async throws where T: Encodable {
60+
func set<T>(_ object: T, for key: String) async throws where T: Encodable {
5761
try requireBackingStore()
5862
return try await backingStore.set(object, for: key)
5963
}

0 commit comments

Comments
 (0)