Kinvey/swift-sdk

View on GitHub
Kinvey/Kinvey/SaveMultiOperation.swift

Summary

Maintainability
F
3 days
Test Coverage
//
//  SaveMultiOperation.swift
//  Kinvey
//
//  Created by Victor Hugo Carvalho Barros on 2019-05-13.
//  Copyright © 2019 Kinvey. All rights reserved.
//

import Foundation
import PromiseKit

internal let maxSizePerRequest = 100

public typealias MultiSaveResultTuple<T> = (entities: AnyRandomAccessCollection<T?>, errors: AnyRandomAccessCollection<Swift.Error>)

internal class SaveMultiOperation<T: Persistable>: WriteOperation<T, MultiSaveResultTuple<T>> where T: NSObject {
    
    let persistable: AnyRandomAccessCollection<T>
    let isNewItems: AnyRandomAccessCollection<Bool>
    let newItems: AnyRandomAccessCollection<T>
    let existingItems: AnyRandomAccessCollection<T>
    
    typealias ResultType = Swift.Result<MultiSaveResultTuple<T>, Swift.Error>
    
    init<C: RandomAccessCollection>(
        persistable: C,
        writePolicy: WritePolicy,
        sync: AnySync? = nil,
        cache: AnyCache<T>? = nil,
        options: Options?
    ) where C.Element == T {
        self.persistable = AnyRandomAccessCollection(persistable)
        var isNewItems = [Bool]()
        var newItems = [T]()
        var existingItems = [T]()
        var isNew: Bool
        isNewItems.reserveCapacity(persistable.count)
        newItems.reserveCapacity(persistable.count)
        existingItems.reserveCapacity(persistable.count)
        for item in persistable {
            isNew = item.isNew
            isNewItems.append(isNew)
            if isNew {
                newItems.append(item)
            } else {
                existingItems.append(item)
            }
        }
        self.isNewItems = AnyRandomAccessCollection(isNewItems)
        self.newItems = AnyRandomAccessCollection(newItems)
        self.existingItems = AnyRandomAccessCollection(existingItems)
        super.init(
            writePolicy: writePolicy,
            sync: sync,
            cache: cache,
            options: options
        )
    }
    
    private func requestBodyCannotBeAnEmptyArrayError(_ completionHandler: CompletionHandler?) -> AnyRequest<ResultType> {
        let error = Error.badRequest(httpResponse: nil, data: nil, description: "Request body cannot be an empty array")
        let result: Swift.Result<MultiSaveResultTuple<T>, Swift.Error> = .failure(error)
        completionHandler?(result)
        return AnyRequest(result)
    }
    
    func executeLocal(_ completionHandler: CompletionHandler?) -> AnyRequest<ResultType> {
        guard self.persistable.count > 0 else {
            return requestBodyCannotBeAnEmptyArrayError(completionHandler)
        }
        let request = LocalRequest<Swift.Result<MultiSaveResultTuple<T>, Swift.Error>>()
        request.execute { () -> Void in
            var isNewArray = [Bool]()
            if let cache = self.cache {
                let persistable = self.persistable.map { (entity: T) -> T in
                    isNewArray.append(entity.isNew)
                    var persistable = entity
                    return self.fillObject(&persistable)
                }
                cache.save(entities: persistable, syncQuery: nil)
            }
            
            if let sync = self.sync {
                let pendingOperations = persistable.enumerated().map { (args) -> PendingOperation in
                    let (offset, item) = args
                    let networkRequest = self.client.networkRequestFactory.appData.buildAppDataSave(
                        item,
                        options: options,
                        isNew: isNewArray[offset],
                        resultType: T.self
                    )
                    return sync.createPendingOperation(networkRequest.request, objectId: item.entityId)
                }
                sync.save(pendingOperations: pendingOperations)
            }
            request.result = .success((entities: AnyRandomAccessCollection(persistable.map({ Optional($0) })), errors: AnyRandomAccessCollection(EmptyCollection())))
            if let completionHandler = completionHandler, let result = request.result {
                completionHandler(result)
            }
        }
        return AnyRequest(request)
    }
    
    func executeNetwork(_ completionHandler: CompletionHandler?) -> AnyRequest<ResultType> {
        guard client.activeUser != nil else {
            let result: Swift.Result<MultiSaveResultTuple<T>, Swift.Error> = .failure(Error.noActiveUser)
            completionHandler?(result)
            return AnyRequest(result)
        }
        guard self.persistable.count > 0 else {
            return requestBodyCannotBeAnEmptyArrayError(completionHandler)
        }
        let requests = MultiRequest<ResultType>()
        
        saveNewItems(requests: requests).then { (newItemsResponse) -> Promise<(MultiSaveResultTuple<T>, [Swift.Result<T, Swift.Error>])> in
            let newItemsResult = try newItemsResponse.get()
            return self.updateExistingItems(requests: requests).map { (newItemsResult, $0) }
        }.done { newItemsResult, existingItemsResult in
            guard existingItemsResult.count > 0 else {
                requests.result = .success(newItemsResult)
                return
            }
            var entities = [T?]()
            var errors = [Swift.Error]()
            let newItemsIterator = newItemsResult.entities.makeIterator()
            var existingItemsIterator = existingItemsResult.makeIterator()
            let newItemsErrorsIterator = newItemsResult.errors.makeIterator()
            for isNew in self.isNewItems {
                if isNew {
                    guard let newItem = newItemsIterator.next() else {
                        continue
                    }
                    entities.append(newItem)
                    if newItem == nil, let error = newItemsErrorsIterator.next() {
                        errors.append(error)
                    }
                } else {
                    guard let existingItem = existingItemsIterator.next() else {
                        continue
                    }
                    switch existingItem {
                    case .success(let existingItem):
                        entities.append(existingItem)
                    case .failure(let error):
                        let error = IndexedError(index: entities.count, error: error)
                        entities.append(nil)
                        errors.append(error)
                    }
                }
            }
            requests.result = .success(
                MultiSaveResultTuple(
                    entities: AnyRandomAccessCollection(entities),
                    errors: AnyRandomAccessCollection(errors)
                )
            )
        }.catch { error in
            requests.result = .failure(error)
        }.finally {
            if let completionHandler = completionHandler, let result = requests.result {
                completionHandler(result)
            }
        }
        
        return AnyRequest(requests)
    }
    
    private func updateExistingItems(requests: MultiRequest<ResultType>) -> Promise<[Swift.Result<T, Swift.Error>]> {
        let iterator = existingItems.makeIterator()
        let promisesIterator = AnyIterator<Promise<Swift.Result<T, Swift.Error>>> {
            guard let persistable = iterator.next() else {
                return nil
            }
            return Promise<Swift.Result<T, Swift.Error>> { resolver in
                let operation = SaveOperation<T>(
                    persistable: persistable,
                    writePolicy: .forceNetwork,
                    sync: self.sync,
                    cache: self.cache,
                    options: self.options
                )
                requests += operation.execute {
                    resolver.fulfill($0)
                }
            }
        }
        let urlSessionConfiguration = options?.urlSession?.configuration ?? client.urlSession.configuration
        return when(fulfilled: promisesIterator, concurrently: urlSessionConfiguration.httpMaximumConnectionsPerHost)
    }
    
    private func saveNewItems(requests: MultiRequest<ResultType>) -> Promise<ResultType> {
        switch newItems.count {
        case 0:
            let result: Swift.Result<MultiSaveResultTuple<T>, Swift.Error> = .success((entities: AnyRandomAccessCollection(EmptyCollection()), errors: AnyRandomAccessCollection(EmptyCollection())))
            return Promise.value(result)
        case 1 ... maxSizePerRequest:
            return saveSingleRequest(newItems: newItems, requests: requests)
        default:
            return saveMultiRequests(newItems: newItems, requests: requests)
        }
    }
    
    private func saveSingleRequest(newItems: AnyRandomAccessCollection<T>, requests: MultiRequest<ResultType>) -> Promise<ResultType> {
        let objectIds = newItems.map({ $0.entityId })
        let request = client.networkRequestFactory.appData.buildAppDataSave(
            newItems,
            options: options,
            resultType: ResultType.self
        )
        requests += request
        return Promise<ResultType> { resolver in
            request.execute() { _data, _response, error in
                guard let response = _response,
                    response.isOK,
                    let data = _data,
                    let json = try? self.client.jsonParser.parseDictionary(from: data),
                    let entitiesJson = json["entities"] as? [JsonDictionary?],
                    let entities = try? entitiesJson.map({ (item) -> T? in
                        guard let item = item else {
                            return nil
                        }
                        return try self.client.jsonParser.parseObject(T.self, from: item)
                    }),
                    let errors = json["errors"] as? [JsonDictionary]
                else {
                    resolver.reject(buildError(_data, _response, error, self.client))
                    return
                }
                for (offset, entity) in entities.enumerated() {
                    guard let entity = entity else {
                        continue
                    }
                    if let objectId = objectIds[offset],
                        let sync = self.sync
                    {
                        sync.removeAllPendingOperations(
                            objectId,
                            methods: ["POST"]
                        )
                    }
                    if let cache = self.cache {
                        if let objectId = objectIds[offset] {
                            cache.remove(byQuery: Query(format: "entityId == %@", objectId))
                        }
                        cache.save(entity: entity)
                    }
                }
                resolver.fulfill(.success((
                    entities: AnyRandomAccessCollection(entities),
                    errors: AnyRandomAccessCollection(errors.map({
                        let json = $0
                        guard let error = json["error"] as? String,
                            let index = json["index"] as? Int
                        else {
                            return Error.unknownJsonError(httpResponse: response.httpResponse, data: data, json: json)
                        }
                        let description = json["description"] as? String
                        let debug = json["debug"] as? String
                        
                        return MultiSaveError(
                            index: index,
                            error: error,
                            serverDescription: description,
                            serverDebug: debug
                        )
                    }))
                )))
            }
        }
    }
    
    private func saveMultiRequests(newItems: AnyRandomAccessCollection<T>, requests: MultiRequest<ResultType>) -> Promise<ResultType> {
        var offsetIterator = stride(from: 0, to: persistable.count, by: maxSizePerRequest).makeIterator()
        let promisesIterator = AnyIterator<Guarantee<ResultType>> {
            guard let offset = offsetIterator.next() else {
                return nil
            }
            let startIndex = newItems.index(newItems.startIndex, offsetBy: offset)
            let endIndex = newItems.index(newItems.startIndex, offsetBy: offset + maxSizePerRequest, limitedBy: newItems.endIndex) ?? newItems.endIndex
            let range = startIndex ..< endIndex
            let slice = newItems[range]
            return self.saveSingleRequest(newItems: slice, requests: requests).recover({ (error) -> Guarantee<ResultType> in
                return Guarantee<ResultType>.value(.success((
                    entities: AnyRandomAccessCollection(Array(repeating: nil, count: slice.count)),
                    errors: AnyRandomAccessCollection((0 ..< slice.count).map({ IndexedError(index: $0, error: error) }))
                )))
            })
        }
        let urlSessionConfiguration = options?.urlSession?.configuration ?? client.urlSession.configuration
        return when(fulfilled: promisesIterator, concurrently: urlSessionConfiguration.httpMaximumConnectionsPerHost).map(on: DispatchQueue.global(qos: .background)) { results -> ResultType in
            var entities = [T?]()
            var errors = [Swift.Error]()
            for result in results {
                switch result {
                case .success(let item):
                    let entitiesCount = entities.count
                    errors.append(contentsOf: item.errors.map {
                        switch $0 {
                        case let multiSaveError as MultiSaveError:
                            return MultiSaveError(
                                index: entitiesCount + multiSaveError.index,
                                error: multiSaveError.error,
                                serverDescription: multiSaveError.serverDescription,
                                serverDebug: multiSaveError.serverDebug
                            )
                        case let indexedError as IndexedError:
                            return IndexedError(
                                index: entitiesCount + indexedError.index,
                                error: indexedError.error
                            )
                        default:
                            return $0
                        }
                    })
                    entities.append(contentsOf: item.entities)
                case .failure(let error):
                    errors.append(contentsOf: [error])
                }
            }
            return .success((entities: AnyRandomAccessCollection(entities), errors: AnyRandomAccessCollection(errors)))
        }
    }
    
}

extension SaveMultiOperation : SaveOperationType {
    
    var localSuccess: MultiSaveResultTuple<T> {
        let entities: [T?] = persistable.map { (entity) -> T? in
            return entity
        }
        return MultiSaveResultTuple(
            entities: AnyRandomAccessCollection(entities),
            errors: AnyRandomAccessCollection([])
        )
    }
    
}