Skip to content

Commit

Permalink
fix: Storage access should wait until SDK initializes (#65)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cbaker6 authored Mar 9, 2023
1 parent 16c9ec6 commit ed55608
Show file tree
Hide file tree
Showing 14 changed files with 255 additions and 90 deletions.
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
# Parse-Swift Changelog

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

### 5.0.1
[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)

__Fixes__
* 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).

### 5.0.0
[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)

__Breaking Changes__
* 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).
* 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).
* 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).
* Added a new ParseHealth.Status enum to support new feature in Parse Server 6.0.0.
Developers can now receive intermediate status updates (Status.initialized, Status.starting)
using the ParseHealth.check callback or Combine methods. Status.initialized and
Expand Down Expand Up @@ -39,7 +44,7 @@ __New features__
* 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).

__Fixes__
* 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).
* 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).
* 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).
* Fixed ambiguous ParseAnalytics trackAppOpenned ([#55](https://github.com/netreconlab/Parse-Swift/pull/55)), thanks to [Corey Baker](https://github.com/cbaker6).
* 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).
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/API/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public struct API {
headers["X-Parse-Session-Token"] = token
}

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

Expand Down
67 changes: 46 additions & 21 deletions Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import FoundationNetworking
```swift
// If "Message" is a "ParseObject"
let myQuery = Message.query("from" == "parse")
guard let subscription = myQuery.subscribe else {
guard let subscription = try await myQuery.subscribe() else {
"Error subscribing..."
return
}
Expand Down Expand Up @@ -93,9 +93,14 @@ public final class ParseLiveQuery: NSObject {
}
}

/// Current LiveQuery client.
public private(set) static var client: ParseLiveQuery?

/// The current status of the LiveQuery socket.
public internal(set) var status: ConnectionStatus = .socketNotEstablished

static var isConfiguring: Bool = false

let notificationQueue: DispatchQueue
var task: URLSessionWebSocketTask!
var url: URL!
Expand Down Expand Up @@ -155,6 +160,7 @@ Not attempting to open ParseLiveQuery socket anymore
try await self.resumeTask()
if isDefault {
Self.defaultClient = self
Self.isConfiguring = false
}
}

Expand All @@ -165,6 +171,38 @@ Not attempting to open ParseLiveQuery socket anymore
}
authenticationDelegate = nil
receiveDelegate = nil
if Self.client == self {
Self.isConfiguring = false
}
}

static func yieldIfNotConfigured() async {
guard !isConfiguring else {
await Task.yield()
await yieldIfNotConfigured()
return
}
}

static func configure() async throws {
guard Self.client == nil else {
return
}
guard !isConfiguring else {
await yieldIfNotConfigured()
return
}
isConfiguring = true
await yieldIfNotInitialized()
Self.defaultClient = try await Self(isDefault: true)
}

static func client() async throws -> ParseLiveQuery {
try await configure()
guard let client = Self.client else {
throw ParseError(code: .otherCause, message: "Missing LiveQuery client")
}
return client
}

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

/// Current LiveQuery client.
public private(set) static var client: ParseLiveQuery?

func resumeTask() async throws {
switch self.task.state {
case .suspended:
Expand Down Expand Up @@ -835,10 +870,7 @@ public extension Query {
- throws: An error of type `ParseError`.
*/
func subscribe() async throws -> Subscription<ResultType> {
guard let client = ParseLiveQuery.client else {
throw ParseError(code: .otherCause, message: "Missing LiveQuery client")
}
return try await client.subscribe(self)
try await ParseLiveQuery.client().subscribe(self)
}

/**
Expand All @@ -862,11 +894,7 @@ public extension Query {
- throws: An error of type `ParseError`.
*/
static func subscribe<T: QuerySubscribable>(_ handler: T) async throws -> T {
if let client = ParseLiveQuery.client {
return try await client.subscribe(handler)
} else {
throw ParseError(code: .otherCause, message: "ParseLiveQuery Error: Not able to initialize client.")
}
try await ParseLiveQuery.client().subscribe(handler)
}

/**
Expand All @@ -877,7 +905,7 @@ public extension Query {
- throws: An error of type `ParseError`.
*/
static func subscribe<T: QuerySubscribable>(_ handler: T, client: ParseLiveQuery) async throws -> T {
try await client.subscribe(handler)
try await ParseLiveQuery.client().subscribe(handler)
}

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

/**
Expand All @@ -913,7 +938,7 @@ public extension Query {
- throws: An error of type `ParseError`.
*/
func unsubscribe() async throws {
try await ParseLiveQuery.client?.unsubscribe(self)
try await ParseLiveQuery.client().unsubscribe(self)
}

/**
Expand All @@ -933,7 +958,7 @@ public extension Query {
- throws: An error of type `ParseError`.
*/
func unsubscribe<T: QuerySubscribable>(_ handler: T) async throws {
try await ParseLiveQuery.client?.unsubscribe(handler)
try await ParseLiveQuery.client().unsubscribe(handler)
}

/**
Expand All @@ -957,7 +982,7 @@ public extension Query {
- throws: An error of type `ParseError`.
*/
func update<T: QuerySubscribable>(_ handler: T) async throws {
try await ParseLiveQuery.client?.update(handler)
try await ParseLiveQuery.client().update(handler)
}

/**
Expand Down
41 changes: 21 additions & 20 deletions Sources/ParseSwift/Objects/ParseInstallation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,23 +204,32 @@ struct CurrentInstallationContainer<T: ParseInstallation>: Codable, Hashable {

// MARK: Current Installation Support
public extension ParseInstallation {

internal static func create() async throws {
let newInstallationId = UUID().uuidString.lowercased()
var newInstallation = BaseParseInstallation()
newInstallation.installationId = newInstallationId
newInstallation.createInstallationId(newId: newInstallationId)
newInstallation.updateAutomaticInfo()
let newBaseInstallationContainer =
CurrentInstallationContainer<BaseParseInstallation>(currentInstallation: newInstallation,
installationId: newInstallationId)
try await ParseStorage.shared.set(newBaseInstallationContainer,
for: ParseStorage.Keys.currentInstallation)
#if !os(Linux) && !os(Android) && !os(Windows)
try? await KeychainStore.shared.set(newBaseInstallationContainer,
for: ParseStorage.Keys.currentInstallation)
#endif
}

internal static func currentContainer() async -> CurrentInstallationContainer<Self> {
guard let installationInMemory: CurrentInstallationContainer<Self> =
try? await ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else {
#if !os(Linux) && !os(Android) && !os(Windows)
guard let installationFromKeyChain: CurrentInstallationContainer<Self> =
try? await KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation)
else {
let newInstallationId = UUID().uuidString.lowercased()
var newInstallation = BaseParseInstallation()
newInstallation.installationId = newInstallationId
newInstallation.createInstallationId(newId: newInstallationId)
newInstallation.updateAutomaticInfo()
let newBaseInstallationContainer =
CurrentInstallationContainer<BaseParseInstallation>(currentInstallation: newInstallation,
installationId: newInstallationId)
try? await KeychainStore.shared.set(newBaseInstallationContainer,
for: ParseStorage.Keys.currentInstallation)
try? await create()
guard let installationFromKeyChain: CurrentInstallationContainer<Self> =
try? await KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation)
else {
Expand All @@ -232,16 +241,7 @@ public extension ParseInstallation {
}
return installationFromKeyChain
#else
let newInstallationId = UUID().uuidString.lowercased()
var newInstallation = BaseParseInstallation()
newInstallation.installationId = newInstallationId
newInstallation.createInstallationId(newId: newInstallationId)
newInstallation.updateAutomaticInfo()
let newBaseInstallationContainer =
CurrentInstallationContainer<BaseParseInstallation>(currentInstallation: newInstallation,
installationId: newInstallationId)
try? await ParseStorage.shared.set(newBaseInstallationContainer,
for: ParseStorage.Keys.currentInstallation)
try? await create()
guard let installationFromMemory: CurrentInstallationContainer<Self> =
try? await ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation)
else {
Expand Down Expand Up @@ -286,6 +286,7 @@ public extension ParseInstallation {
- throws: An error of `ParseError` type.
*/
static func current() async throws -> Self {
await yieldIfNotInitialized()
guard let installation = await Self.currentContainer().currentInstallation else {
throw ParseError(code: .otherCause,
message: "There is no current Installation")
Expand Down
1 change: 1 addition & 0 deletions Sources/ParseSwift/Objects/ParseUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public extension ParseUser {
- throws: An error of `ParseError` type.
*/
static func current() async throws -> Self {
await yieldIfNotInitialized()
guard let container = await Self.currentContainer(),
let user = container.currentUser else {
throw ParseError(code: .otherCause,
Expand Down
22 changes: 15 additions & 7 deletions Sources/ParseSwift/Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,20 @@ internal func initialize(applicationId: String,
try await initialize(configuration: configuration)
}

internal func yieldIfNotInitialized() async {
guard ParseConfiguration.checkIfConfigured() else {
await Task.yield()
await yieldIfNotInitialized()
return
}
}

internal func deleteKeychainIfNeeded() async {
#if !os(Linux) && !os(Android) && !os(Windows)
// Clear items out of the Keychain on app first run.
if UserDefaults.standard.object(forKey: ParseConstants.bundlePrefix) == nil {
if Parse.configuration.isDeletingKeychainIfNeeded {
try? await KeychainStore.old.deleteAll()
try? await KeychainStore.old?.deleteAll()
try? await KeychainStore.shared.deleteAll()
}
Parse.configuration.keychainAccessGroup = .init()
Expand Down Expand Up @@ -137,7 +145,7 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif
}
} catch {
// Migrate old installations made with ParseSwift < 1.3.0
if let currentInstallation = try? await BaseParseInstallation.current() {
if let currentInstallation = await BaseParseInstallation.currentContainer().currentInstallation {
if currentInstallation.objectId == nil {
await BaseParseInstallation.deleteCurrentContainerFromKeychain()
// Prepare installation
Expand Down Expand Up @@ -166,23 +174,22 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif
await BaseParseInstallation.createNewInstallationIfNeeded()

#if !os(Linux) && !os(Android) && !os(Windows)
ParseLiveQuery.defaultClient = try await ParseLiveQuery(isDefault: true)
if configuration.isMigratingFromObjcSDK {
await KeychainStore.createObjectiveC()
if let objcParseKeychain = KeychainStore.objectiveC {
guard let installationId: String = await objcParseKeychain.objectObjectiveC(forKey: "installationId"),
try await BaseParseInstallation.current().installationId != installationId else {
currentInstallationContainer.installationId != installationId else {
Parse.configuration.isInitialized = true
return
}
var updatedInstallation = try await BaseParseInstallation.current()
updatedInstallation.installationId = installationId
var currentInstallationContainer = await BaseParseInstallation.currentContainer()
currentInstallationContainer.installationId = installationId
currentInstallationContainer.currentInstallation = updatedInstallation
currentInstallationContainer.currentInstallation?.installationId = installationId
await BaseParseInstallation.setCurrentContainer(currentInstallationContainer)
}
}
#endif
Parse.configuration.isInitialized = true
}

/**
Expand Down Expand Up @@ -340,6 +347,7 @@ public func deleteObjectiveCKeychain() async throws {
throw ParseError(code: .otherCause,
message: "\"accessGroup\" must be set to a valid string when \"synchronizeAcrossDevices == true\"")
}
await yieldIfNotInitialized()
guard let currentAccessGroup = try? await ParseKeychainAccessGroup.current() else {
throw ParseError(code: .otherCause,
message: "Problem unwrapping the current access group. Did you initialize the SDK before calling this method?")
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/ParseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

enum ParseConstants {
static let sdk = "swift"
static let version = "5.0.0"
static let version = "5.0.1"
static let fileManagementDirectory = "parse/"
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
static let fileManagementLibraryDirectory = "Library/"
Expand Down
16 changes: 10 additions & 6 deletions Sources/ParseSwift/Storage/ParseStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

// MARK: ParseStorage
actor ParseStorage {
public static var shared = ParseStorage()
static var shared = ParseStorage()

private var backingStore: ParsePrimitiveStorable!
var backingStore: ParsePrimitiveStorable!

func use(_ store: ParsePrimitiveStorable) {
self.backingStore = store
Expand All @@ -33,27 +33,31 @@ actor ParseStorage {
static let currentVersion = "_currentVersion"
static let currentAccessGroup = "_currentAccessGroup"
}

func setBackingStoreToNil() {
backingStore = nil
}
}

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

public func delete(valueFor key: String) async throws {
func delete(valueFor key: String) async throws {
try requireBackingStore()
return try await backingStore.delete(valueFor: key)
}

public func deleteAll() async throws {
func deleteAll() async throws {
try requireBackingStore()
return try await backingStore.deleteAll()
}

public func get<T>(valueFor key: String) async throws -> T? where T: Decodable {
func get<T>(valueFor key: String) async throws -> T? where T: Decodable {
try requireBackingStore()
return try await backingStore.get(valueFor: key)
}

public func set<T>(_ object: T, for key: String) async throws where T: Encodable {
func set<T>(_ object: T, for key: String) async throws where T: Encodable {
try requireBackingStore()
return try await backingStore.set(object, for: key)
}
Expand Down
Loading

0 comments on commit ed55608

Please sign in to comment.