From 3dc05bea61e79472204f4884c16a538cf43941fc Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 17 Oct 2023 16:26:38 -0400 Subject: [PATCH 1/6] Reset optional and required module mapping at the time of request. --- .../ActionHandling/ActionOpenPageHandler.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift index 989971a..2f79085 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift @@ -11,9 +11,19 @@ import Foundation open class ActionOpenPageHandler: MVMCoreJSONActionHandlerProtocol { required public init() {} + func requestParamaters(for model: ActionOpenPageModel) -> MVMCoreRequestParameters { + let requestParameters = model.requestParameters.copy() as! MVMCoreRequestParameters + if let pageType = requestParameters.pageType { + // Re-evaluate required & optional modules as action models might have been generated prior to recent additions to the mapping. + requestParameters.modules = MVMCoreViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType) as? [String] + requestParameters.optionalModules = MVMCoreViewControllerMappingObject.shared()?.modulesOptional(forPageType: pageType) as? [String] + } + return requestParameters + } + open func performAction(with JSON: [AnyHashable : Any], model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { guard let model = model as? ActionOpenPageModel else { return } - let requestParameters: MVMCoreRequestParameters = model.requestParameters.copy() as! MVMCoreRequestParameters + let requestParameters = requestParamaters(for: model) do { if let closure = delegateObject?.actionDelegate?.handleOpenPage { // Legacy code will use the old handler function and break the task chain here. From 1e8f7f417eb6d298d1f300507da3391adfe12142 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Tue, 17 Oct 2023 16:26:57 -0400 Subject: [PATCH 2/6] HARD_CODED_RESPONSE_DELAY for simulations. --- MVMCore/MVMCore/LoadHandling/MVMCoreLoadRequestOperation.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MVMCore/MVMCore/LoadHandling/MVMCoreLoadRequestOperation.m b/MVMCore/MVMCore/LoadHandling/MVMCoreLoadRequestOperation.m index d6e352b..f0cc338 100644 --- a/MVMCore/MVMCore/LoadHandling/MVMCoreLoadRequestOperation.m +++ b/MVMCore/MVMCore/LoadHandling/MVMCoreLoadRequestOperation.m @@ -299,6 +299,9 @@ if ([[MVMCoreObject sharedInstance].globalLoadDelegate respondsToSelector:@selector(getJSONForRequestParameters:)]) { NSDictionary *json = [[MVMCoreObject sharedInstance].globalLoadDelegate getJSONForRequestParameters:requestParameters]; if (json) { +#if HARD_CODED_RESPONSE_DELAY > 0 + [NSThread sleepForTimeInterval:HARD_CODED_RESPONSE_DELAY]; +#endif completionHandler(json); return; } From d44235bd036894d23d4f7c9eb7d497606bb579f2 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Thu, 19 Oct 2023 12:30:13 -0400 Subject: [PATCH 3/6] Action Share update --- .../ActionHandling/ActionShareHandler.swift | 14 +--- .../ActionHandling/ActionShareModel.swift | 84 +++++++++++++++++-- 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift b/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift index e82baa2..c6e699a 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift @@ -14,15 +14,7 @@ open class ActionShareHandler: MVMCoreActionHandlerProtocol { open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) async throws { guard let model = model as? ActionShareModel else { return } - var shareData: [Any] - switch model.sharedType { - case .text: - shareData = [model.sharedText] - case .url: - let url = try URL.createURL(with: model.sharedText) - shareData = [url] - } - try await shareWith(activityItems: shareData, model: model) + try await shareWith(activityItems: model.items.map { $0.value }, model: model) } @MainActor @@ -33,10 +25,6 @@ open class ActionShareHandler: MVMCoreActionHandlerProtocol { controller.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in if completed { // Activity was completed, considered finished. - if activityType == .copyToPasteboard { - // Allow copy - MVMCoreSessionObject.sharedGlobal()?.copyString(toClipboard: model.sharedText) - } continuation.resume() } else if let _ = activityType { // If a specific type of activity failed, the activity controller is still presented, cannot continue yet. diff --git a/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift b/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift index 2203a8c..7895c68 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift @@ -6,14 +6,51 @@ // Copyright © 2020 myverizon. All rights reserved. // - -public struct ActionShareModel: ActionModelProtocol { +public struct ActionShareItemModel: Codable { public enum SharedType: String, Codable { case text case url } + public var type: SharedType + public var value: Any + + private enum CodingKeys: String, CodingKey { + case type + case value + } + + public init(type: SharedType, value: Any) { + self.type = type + self.value = value + } + + public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + type = try typeContainer.decode(SharedType.self, forKey: .type) + switch type { + case .text: + value = try typeContainer.decode(String.self, forKey: .value) + case .url: + value = try typeContainer.decode(URL.self, forKey: .value) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + switch type { + case .text: + try container.encode(value as! String, forKey: .value) + case .url: + try container.encode(value as! URL, forKey: .value) + } + } +} + +public struct ActionShareModel: ActionModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -21,8 +58,7 @@ public struct ActionShareModel: ActionModelProtocol { public static var identifier: String = "share" public var actionType: String = ActionShareModel.identifier - public var sharedType: SharedType - public var sharedText: String + public var items: [ActionShareItemModel] public var extraParameters: JSONValueDictionary? public var analyticsData: JSONValueDictionary? @@ -30,10 +66,44 @@ public struct ActionShareModel: ActionModelProtocol { // MARK: - Initializer //-------------------------------------------------- - public init(sharedText: String, sharedType: SharedType, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { - self.sharedType = sharedType - self.sharedText = sharedText + public init(sharedText: String, sharedType: ActionShareItemModel.SharedType, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { + items = [ActionShareItemModel(type: sharedType, value: sharedText)] self.extraParameters = extraParameters self.analyticsData = analyticsData } + + //-------------------------------------------------- + // MARK: - Codable + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case actionType + case items + case sharedType + case sharedText + } + + public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + if let items = try typeContainer.decodeIfPresent([ActionShareItemModel].self, forKey: .items) { + self.items = items + } else { + // Legacy + let type = try typeContainer.decode(ActionShareItemModel.SharedType.self, forKey: .sharedType) + var value: Any + switch type { + case .url: + value = try typeContainer.decode(URL.self, forKey: .sharedText) + default: + value = try typeContainer.decode(String.self, forKey: .sharedText) + } + items = [ActionShareItemModel(type: type, value: value)] + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(actionType, forKey: .actionType) + try container.encode(items, forKey: .items) + } } From def76778e52dc0ddbfb81e2a43506c966f268f7b Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 20 Oct 2023 11:51:14 -0400 Subject: [PATCH 4/6] Code review updates for scoping of functions. Fix continuation leak. --- .../ActionHandling/ActionShareHandler.swift | 6 ---- .../ActionHandling/ActionShareModel.swift | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift b/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift index c6e699a..3634c11 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionShareHandler.swift @@ -26,12 +26,6 @@ open class ActionShareHandler: MVMCoreActionHandlerProtocol { if completed { // Activity was completed, considered finished. continuation.resume() - } else if let _ = activityType { - // If a specific type of activity failed, the activity controller is still presented, cannot continue yet. - if let error = error, - let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: MVMCoreActionHandler.getErrorLocation(with: delegateObject?.actionDelegate, actionType: model.actionType)) { - MVMCoreLoggingHandler.addError(toLog: errorObject) - } } else if let error = error { continuation.resume(throwing: error) } else { diff --git a/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift b/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift index 7895c68..ddeb7d8 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionShareModel.swift @@ -66,8 +66,8 @@ public struct ActionShareModel: ActionModelProtocol { // MARK: - Initializer //-------------------------------------------------- - public init(sharedText: String, sharedType: ActionShareItemModel.SharedType, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { - items = [ActionShareItemModel(type: sharedType, value: sharedText)] + public init(items: [ActionShareItemModel], _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { + self.items = items self.extraParameters = extraParameters self.analyticsData = analyticsData } @@ -83,24 +83,34 @@ public struct ActionShareModel: ActionModelProtocol { case sharedText } + private enum DeprecatedCodingKeys: String, CodingKey { + case sharedType + case sharedText + } + public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) if let items = try typeContainer.decodeIfPresent([ActionShareItemModel].self, forKey: .items) { - self.items = items + self.init(items: items) } else { // Legacy - let type = try typeContainer.decode(ActionShareItemModel.SharedType.self, forKey: .sharedType) - var value: Any - switch type { - case .url: - value = try typeContainer.decode(URL.self, forKey: .sharedText) - default: - value = try typeContainer.decode(String.self, forKey: .sharedText) - } - items = [ActionShareItemModel(type: type, value: value)] + try self.init(deprecatedFrom: decoder) } } + private init(deprecatedFrom decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: DeprecatedCodingKeys.self) + let type = try typeContainer.decode(ActionShareItemModel.SharedType.self, forKey: .sharedType) + var value: Any + switch type { + case .url: + value = try typeContainer.decode(URL.self, forKey: .sharedText) + default: + value = try typeContainer.decode(String.self, forKey: .sharedText) + } + items = [ActionShareItemModel(type: type, value: value)] + } + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(actionType, forKey: .actionType) From 2a438e68a71e906cb2c1c45de627d9a138fe2ab8 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 20 Oct 2023 12:04:28 -0400 Subject: [PATCH 5/6] Improve MVMCoreActionUtility.perform signature. Remove odd default. Use TimeInterval for better typing and to allow split seconds. --- .../Client Parameters/ClientParameterHandler.swift | 2 +- .../Client Parameters/ClientParameterProtocol.swift | 8 +++----- .../Utility/Helpers/MVMCoreActionUtility+Extension.swift | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index bd1db37..18748f9 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -106,7 +106,7 @@ parametersWorkQueue.async { if (returnedList[index] != nil) { MVMCoreLoggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) - } else { + } else { MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) returnedList[index] = receivedParameter group.leave() // Leaving is only done after setup (barriered). diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift index 7f52778..e55fd15 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift @@ -15,7 +15,7 @@ public protocol ClientParameterProtocol: ModelHandlerProtocol { var clientParameterModel: ClientParameterModelProtocol { get set } - func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: Double, completionHandler:@escaping ([String: AnyHashable]?) -> ()) + func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completionHandler:@escaping ([String: AnyHashable]?) -> ()) /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. func valueOnTimeout() -> [String: AnyHashable] @@ -28,10 +28,8 @@ public extension ClientParameterProtocol { } /// The handler should call this method to pass the parameter back to the caller. - func returnParameters(_ isFlatMap: Bool, _ parameter: [String: AnyHashable]?, completionHandler: @escaping ([String: AnyHashable]?) -> ()) { - guard let parameter = parameter else { - return completionHandler(nil) - } + /// If using isFlatMap, you must provide at least 1 element in parameters or it will result in triggering a timeout. + func returnParameters(_ isFlatMap: Bool, _ parameter: [String: AnyHashable], completionHandler: @escaping ([String: AnyHashable]?) -> ()) { if isFlatMap { completionHandler(parameter) } else { diff --git a/MVMCore/MVMCore/Utility/Helpers/MVMCoreActionUtility+Extension.swift b/MVMCore/MVMCore/Utility/Helpers/MVMCoreActionUtility+Extension.swift index 10299bb..bb6e590 100644 --- a/MVMCore/MVMCore/Utility/Helpers/MVMCoreActionUtility+Extension.swift +++ b/MVMCore/MVMCore/Utility/Helpers/MVMCoreActionUtility+Extension.swift @@ -27,7 +27,7 @@ public extension MVMCoreActionUtility { - Parameter timemout: the time the operation has to finish before throwing a timeout error. - Parameter operation: the operation to perform. */ - static func perform(with timeout: Int = 1, operation: @escaping @Sendable () async throws -> T) async throws -> T { + static func perform(withTimeout timeout: TimeInterval, operation: @escaping @Sendable () async throws -> T) async throws -> T { return try await withCheckedThrowingContinuation { continuation in Task { await withThrowingTaskGroup(of: T.self) { group in @@ -40,7 +40,7 @@ public extension MVMCoreActionUtility { // Task for time out. group.addTask { try Task.checkCancellation() - try await Task.sleep(nanoseconds: UInt64(timeout) * NSEC_PER_SEC) + try await Task.sleep(nanoseconds: UInt64(timeout * TimeInterval(NSEC_PER_SEC))) throw MVMCoreActionUtilityError.timeOut } From f7ca8da8f94c21d97656be45eb6516ce5efa4c45 Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 20 Oct 2023 13:38:39 -0400 Subject: [PATCH 6/6] Add additional task completion guard to suppress late callbacks. --- .../ClientParameterHandler.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 18748f9..6f8c7e0 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -104,13 +104,17 @@ timingOutIn: timeout) { (receivedParameter) in // Queue the results for merge. parametersWorkQueue.async { - if (returnedList[index] != nil) { - MVMCoreLoggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) - } else { - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) - returnedList[index] = receivedParameter - group.leave() // Leaving is only done after setup (barriered). + guard !complete else { + MVMCoreLoggingHandler.logDebugMessage(withDelegate: "Client \(parameterType) responded after task completion.") + return } + guard returnedList[index] == nil else { + MVMCoreLoggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) + return + } + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) + returnedList[index] = receivedParameter + group.leave() // Leaving is only done after setup (barriered). } } }