8000 Use Swift Concurrency instead of PromiseKit by rgoldberg · Pull Request #737 · mas-cli/mas · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Use Swift Concurrency instead of PromiseKit #737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,6 @@
"version" : "13.7.1"
}
},
{
"identity" : "promisekit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mxcl/PromiseKit.git",
"state" : {
"revision" : "2bc44395edb4f8391902a9ff7c220471882a4d07",
"version" : "8.2.0"
}
},
{
"identity" : "quick",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 0 additions & 2 deletions Package.swift
8000
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ let package = Package(
.package(url: "https://github.com/Quick/Quick.git", from: "7.6.2"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
.package(url: "https://github.com/funky-monkey/IsoCountryCodes.git", from: "1.0.2"),
.package(url: "https://github.com/mxcl/PromiseKit.git", from: "8.2.0"),
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
],
Expand All @@ -33,7 +32,6 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"IsoCountryCodes",
"PromiseKit",
"Regex",
"Version",
],
Expand Down
101 changes: 40 additions & 61 deletions Sources/mas/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,93 +6,72 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
//

import PromiseKit
import StoreFoundation

/// Sequentially downloads apps, printing progress to the console.
///
/// Verifies that each supplied app ID is valid before attempting to download.
///
/// - Parameters:
/// - unverifiedAppIDs: The app IDs of the apps to be verified and downloaded.
/// - appIDs: The app IDs of the apps to be verified and downloaded.
/// - searcher: The `AppStoreSearcher` used to verify app IDs.
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
/// - Returns: A `Promise` that completes when the downloads are complete. If any fail,
/// the promise is rejected with the first error, after all remaining downloads are attempted.
/// - Throws: If any download fails, immediately throws an error.
func downloadApps(
withAppIDs unverifiedAppIDs: [AppID],
withAppIDs appIDs: [AppID],
verifiedBy searcher: AppStoreSearcher,
purchasing: Bool = false
) -> Promise<Void> {
when(resolved: unverifiedAppIDs.map { searcher.lookup(appID: $0) })
.then { results in
downloadApps(
withAppIDs:
results.compactMap { result in
switch result {
case .fulfilled(let searchResult):
return searchResult.trackId
case .rejected(let error):
printError(String(describing: error))
return nil
}
< AE27 /td> },
purchasing: purchasing
)
) async throws {
for appID in appIDs {
do {
_ = try await searcher.lookup(appID: appID)
} catch {
guard case MASError.unknownAppID = error else {
throw error
}

printWarning("App ID \(appID) not found in Mac App Store.")
continue
}
try await downloadApp(withAppID: appID, purchasing: purchasing)
}
}

/// Sequentially downloads apps, printing progress to the console.
///
/// - Parameters:
/// - appIDs: The app IDs of the apps to be downloaded.
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
/// - Returns: A promise that completes when the downloads are complete. If any fail,
/// the promise is rejected with the first error, after all remaining downloads are attempted.
func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) -> Promise<Void> {
var firstError: Error?
return
appIDs
.reduce(Guarantee.value(())) { previous, appID in
previous.then {
downloadApp(withAppID: appID, purchasing: purchasing)
.recover { error in
if firstError == nil {
firstError = error
}
}
}
}
.done {
if let firstError {
throw firstError
}
}
/// - Throws: If a download fails, immediately throws an error.
func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) async throws {
for appID in appIDs {
try await downloadApp(withAppID: appID, purchasing: purchasing)
}
}

private func downloadApp(
withAppID appID: AppID,
purchasing: Bool = false,
withAttemptCount attemptCount: UInt32 = 3
) -> Promise<Void> {
SSPurchase()
.perform(appID: appID, purchasing: purchasing)
.recover { error in
guard attemptCount > 1 else {
throw error
}

// If the download failed due to network issues, try again. Otherwise, fail immediately.
guard
case MASError.downloadFailed(let downloadError) = error,
case NSURLErrorDomain = downloadError?.domain
else {
throw error
}
) async throws {
do {
try await SSPurchase().perform(appID: appID, purchasing: purchasing)
} catch {
guard attemptCount > 1 else {
throw error
}

let attemptCount = attemptCount - 1
printWarning((downloadError ?? error).localizedDescription)
printWarning("Trying again up to \(attemptCount) more \(attemptCount == 1 ? "time" : "times").")
return downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
// If the download failed due to network issues, try again. Otherwise, fail immediately.
guard
case MASError.downloadFailed(let downloadError) = error,
case NSURLErrorDomain = downloadError?.domain
else {
throw error
}

let attemptCount = attemptCount - 1
printWarning((downloadError ?? error).localizedDescription)
printWarning("Trying again up to \(attemptCount) more \(attemptCount == 1 ? "time" : "times").")
try await downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
}
}
28 changes: 10 additions & 18 deletions Sources/mas/AppStore/ISStoreAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,21 @@
//

import CommerceKit
import PromiseKit

private let timeout = 30.0

extension ISStoreAccount {
static var primaryAccount: Promise<ISStoreAccount> {
race(
Promise { seal in
ISServiceProxy.genericShared().accountService
.primaryAccount { storeAccount in
seal.fulfill(storeAccount)
}
},
after(seconds: timeout)
.then {
Promise(error: MASError.notSignedIn)
}
)
static var primaryAccount: ISStoreAccount {
get throws {
guard let account = ISServiceProxy.genericShared().storeClient?.primaryAccount else {
throw MASError.notSignedIn
}

return account
}
}

static func signIn(appleID _: String, password _: String, systemDialog _: Bool) -> Promise<ISStoreAccount> {
static func signIn(appleID _: String, password _: String, systemDialog _: Bool) throws -> ISStoreAccount {
// Signing in is no longer possible as of High Sierra.
// https://github.com/mas-cli/mas/issues/164
Promise(error: MASError.notSupported)
throw MASError.notSupported
}
}
12 changes: 5 additions & 7 deletions Sources/mas/AppStore/PurchaseDownloadObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

import CommerceKit
import PromiseKit

private let downloadingPhase = 0 as Int64
private let installingPhase = 1 as Int64
Expand Down Expand Up @@ -134,15 +133,14 @@ private extension SSDownloadPhase {
}

extension PurchaseDownloadObserver {
func observeDownloadQueue(_ downloadQueue: CKDownloadQueue = CKDownloadQueue.shared()) -> Promise<Void> {
func observeDownloadQueue(_ downloadQueue: CKDownloadQueue = CKDownloadQueue.shared()) {
let observerID = downloadQueue.add(self)

return Promise<Void> { seal in
errorHandler = seal.reject
completionHandler = seal.fulfill_
completionHandler = {
downloadQueue.remove(observerID)
}
.ensure {
errorHandler = { _ in
downloadQueue.remove(observerID)
// ROSS: throw error handler argument
}
}
}
39 changes: 14 additions & 25 deletions Sources/mas/AppStore/SSPurchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
//

import CommerceKit
import PromiseKit

extension SSPurchase {
func perform(appID: AppID, purchasing: Bool) -> Promise<Void> {
func perform(appID: AppID, purchasing: Bool) async throws {
var parameters =
[
"productType": "C",
Expand Down Expand Up @@ -44,38 +43,28 @@ extension SSPurchase {
// Monterey obscures the user's App Store account, but allows
// redownloads without passing any account IDs to SSPurchase.
// https://github.com/mas-cli/mas/issues/417
if #available(macOS 12, *) {
return perform()
if #unavailable(macOS 12) {
let storeAccount = try ISStoreAccount.primaryAccount
accountIdentifier = storeAccount.dsID
appleID = storeAccount.identifier
}

return
ISStoreAccount.primaryAccount
.then { storeAccount in
self.accountIdentifier = storeAccount.dsID
self.appleID = storeAccount.identifier
return self.perform()
}
try await perform()
}

private func perform() -> Promise<Void> {
Promise<SSPurchase> { seal in
private func perform() async throws {
let _: Void = try await withCheckedThrowingContinuation { continuation in
CKPurchaseController.shared()
.perform(self, withOptions: 0) { purchase, _, error, response in
if let error {
seal.reject(MASError.purchaseFailed(error: error as NSError?))
return
}

guard response?.downloads.isEmpty == false, let purchase else {
seal.reject(MASError.noDownloads)
return
continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError?))
} else if response?.downloads.isEmpty == false, let purchase {
PurchaseDownloadObserver(purchase: purchase).observeDownloadQueue()
continuation.resume()
} else {
continuation.resume(throwing: MASError.noDownloads)
}

seal.fulfill(purchase)
}
}
.then { purchase in
PurchaseDownloadObserver(purchase: purchase).observeDownloadQueue()
}
}
}
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension MAS {
}

do {
print(try ISStoreAccount.primaryAccount.wait().identifier)
print(try ISStoreAccount.primaryAccount.identifier)
} catch {
throw error as? MASError ?? MASError.failed(error: error as NSError)
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/mas/Commands/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
extension MAS {
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
/// https://performance-partners.apple.com/search-api
struct Home: ParsableCommand {
struct Home: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Open app's Mac App Store web page in the default web browser"
)
Expand All @@ -21,18 +21,18 @@ extension MAS {
var appID: AppID

/// Runs the command.
func run() throws {
try run(searcher: ITunesSearchAppStoreSearcher())
func run() async throws {
try await run(searcher: ITunesSearchAppStoreSearcher())
}

func run(searcher: AppStoreSearcher) throws {
let result = try searcher.lookup(appID: appID).wait()
func run(searcher: AppStoreSearcher) async throws {
let result = try await searcher.lookup(appID: appID)

guard let url = URL(string: result.trackViewUrl) else {
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
}

try url.open().wait()
try await url.open()
}
}
}
10 changes: 5 additions & 5 deletions Sources/mas/Commands/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ArgumentParser
extension MAS {
/// Displays app details. Uses the iTunes Lookup API:
/// https://performance-partners.apple.com/search-api
struct Info: ParsableCommand {
struct Info: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Display app information from the Mac App Store"
)
Expand All @@ -20,13 +20,13 @@ extension MAS {
var appID: AppID

/// Runs the command.
func run() throws {
try run(searcher: ITunesSearchAppStoreSearcher())
func run() async throws {
try await run(searcher: ITunesSearchAppStoreSearcher())
}

func run(searcher: AppStoreSearcher) throws {
func run(searcher: AppStoreSearcher) async throws {
do {
print(AppInfoFormatter.format(app: try searcher.lookup(appID: appID).wait()))
print(AppInfoFormatter.format(app: try await searcher.lookup(appID: appID)))
} catch {
throw error as? MASError ?? .searchFailed
}
Expand Down
Loading
0