amplify-swift/Amplify/Core/Support/AmplifyOperation.swift

213 lines
8.7 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Combine
import Foundation
/// An abstract representation of an Amplify unit of work. Subclasses may aggregate multiple work items
/// to fulfull a single "AmplifyOperation", such as an "extract text operation" which might include
/// uploading an image to cloud storage, processing it via a Predictions engine, and translating the results.
///
/// AmplifyOperations are used by plugin developers to perform tasks on behalf of the calling app. They have a default
/// implementation of a `dispatch` method that sends a contextualized payload to the Hub.
///
/// Pausable/resumable tasks that do not require Hub dispatching should use AsynchronousOperation instead.
open class AmplifyOperation<Request: AmplifyOperationRequest, Success, Failure: AmplifyError>: AsynchronousOperation {
/// The concrete Request associated with this operation
public typealias Request = Request
/// The concrete Success type associated with this operation
public typealias Success = Success
/// The concrete Failure type associated with this operation
public typealias Failure = Failure
/// Convenience typealias defining the `Result`s dispatched by this operation
public typealias OperationResult = Result<Success, Failure>
/// Convenience typealias for the `listener` callback submitted during Operation creation
public typealias ResultListener = (OperationResult) -> Void
/// The unique ID of the operation. In categories where operations are persisted for future processing, this id can
/// be used to identify previously-scheduled work for progress tracking or other functions.
public let id: UUID
/// Incoming parameters of the original request
public let request: Request
/// All AmplifyOperations must be associated with an Amplify Category
public let categoryType: CategoryType
/// All AmplifyOperations must declare a HubPayloadEventName
public let eventName: HubPayloadEventName
private var resultListenerUnsubscribeToken: UnsubscribeToken?
/// Local storage for the result publisher associated with this operation. We use a
/// Future here to ensure that a subscriber will always receive a value, even if
/// the operation has already completed execution by the time the subscriber is
/// attached. We derive the `resultPublisher` computed property from this value.
/// Amplify V2 can expect Combine to be available.
#if canImport(Combine)
var resultFuture: Future<Success, Failure>!
/// Local storage for the result promise associated with this operation. We use
/// this promise handle to resolve the operation in the `dispatch` method
var resultPromise: Future<Success, Failure>.Promise!
#endif
/// Creates an AmplifyOperation for the specified reequest.
///
/// ## Events
/// An AmplifyOperation will dispatch messages to the Hub as it completes its work. The HubPayload for these
/// messages will have the following structure:
/// - **`eventName`**: The event name defined by the operation , such as "Storage.getURL" or "Storage.downloadFile".
/// See `HubPayload.EventName` for a list of pre-defined event names.
/// - **`context`**: An `AmplifyOperationContext` whose `operationId` will be the ID of this operation, and whose
/// `request` will be the Request used to create the operation.
/// - **`data`**: The `OperationResult` that will be dispatched to an event listener. Event types for the listener
/// are derived from the request.
///
/// A caller may specify a listener during a call to an
/// Amplify category API:
/// ```swift
/// Amplify.Storage.list { event in print(event) }
/// ```
///
/// Or after the fact, by passing the operation to the Hub:
/// ```swift
/// Amplify.Hub.listen(to: operation) { event in print(event) }
/// ```
///
/// In either of these cases, Amplify creates a HubListener for the operation by:
/// 1. Filtering messages by the operation's ID
/// 1. Extracting the HubPayload's `data` element and casts it to the expected `OperationResult` type for the
/// listener
/// 1. Automatically unsubscribing the listener (by calling `Amplify.Hub.removeListener`) when the listener receives
/// a result
///
/// Callers can remove the listener at any time by calling `operation.removeResultListener()`.
///
/// - Parameter categoryType: The categoryType of this operation
/// - Parameter eventName: The event name of this operation, used in HubPayload messages dispatched by the operation
/// - Parameter request: The request used to generate this operation
/// - Parameter resultListener: The optional listener for the OperationResults associated with the operation
public init(categoryType: CategoryType,
eventName: HubPayloadEventName,
request: Request,
resultListener: ResultListener? = nil) {
self.categoryType = categoryType
self.eventName = eventName
self.request = request
self.id = UUID()
super.init()
#if canImport(Combine)
resultFuture = Future<Success, Failure> { self.resultPromise = $0 }
#endif
if let resultListener = resultListener {
self.resultListenerUnsubscribeToken = subscribe(resultListener: resultListener)
}
}
func subscribe(resultListener: @escaping ResultListener) -> UnsubscribeToken {
let channel = HubChannel(from: categoryType)
let filterById = HubFilters.forOperation(self)
var token: UnsubscribeToken?
let resultHubListener: HubListener = { payload in
guard let result = payload.data as? OperationResult else {
return
}
resultListener(result)
// Automatically unsubscribe when event is received
guard let token = token else {
return
}
Amplify.Hub.removeListener(token)
}
token = Amplify.Hub.listen(to: channel, isIncluded: filterById, listener: resultHubListener)
// We know that `token` is assigned by `Amplify.Hub.listen` so it's safe to force-unwrap
return token!
}
/// Classes that override this method must emit a completion to the `resultPublisher` upon cancellation
open override func cancel() {
super.cancel()
#if canImport(Combine)
let cancellationError = Failure(
errorDescription: "Operation cancelled",
recoverySuggestion: "The operation was cancelled before it completed",
error: OperationCancelledError()
)
publish(result: .failure(cancellationError))
#endif
removeResultListener()
}
/// Dispatches an event to the hub. Internally, creates an
/// `AmplifyOperationContext` object from the operation's `id`, and `request`. On
/// iOS 13+, this method also publishes the result on the `resultPublisher`.
///
/// - Parameter result: The OperationResult to dispatch to the hub as part of the
/// HubPayload
public func dispatch(result: OperationResult) {
#if canImport(Combine)
publish(result: result)
#endif
let channel = HubChannel(from: categoryType)
let context = AmplifyOperationContext(operationId: id, request: request)
let payload = HubPayload(eventName: eventName, context: context, data: result)
Amplify.Hub.dispatch(to: channel, payload: payload)
}
/// Removes the listener that was registered during operation instantiation
public func removeResultListener() {
guard let unsubscribeToken = resultListenerUnsubscribeToken else {
return
}
Amplify.Hub.removeListener(unsubscribeToken)
resultListenerUnsubscribeToken = nil
}
}
/// All AmplifyOperations must be associated with an Amplify Category
extension AmplifyOperation: CategoryTypeable { }
/// All AmplifyOperations must declare a HubPayloadEventName. Subclasses should provide names by extending
/// `HubPayload.EventName`, e.g.:
///
/// ```
/// public extension HubPayload.EventName.Storage {
/// static let put = "Storage.put"
/// }
/// ```
extension AmplifyOperation: HubPayloadEventNameable { }
/// Conformance to Cancellable we gain for free by subclassing AsynchronousOperation
extension AmplifyOperation: Cancellable { }
/// Describes the parameters that are passed during the creation of an AmplifyOperation
public protocol AmplifyOperationRequest {
/// The concrete Options type that adjusts the behavior of the request type
associatedtype Options
/// Options to adjust the behavior of this request, including plugin options
var options: Options { get }
}