diff --git a/Sources/MeiliSearch/Client.swift b/Sources/MeiliSearch/Client.swift index 6843842c..89418f26 100755 --- a/Sources/MeiliSearch/Client.swift +++ b/Sources/MeiliSearch/Client.swift @@ -373,6 +373,26 @@ public struct MeiliSearch { self.updates.getAll(UID, completion) } + /** + Wait for an update to be processed or failed. + + Providing an update id, returned by asynchronous MeiliSearch options, call are made + to MeiliSearch to check if the update has been processed or if it has failed. + + - parameter UID: The unique identifier of the `Index`. + - parameter updateId: The id of the update. + - parameter: options Optionnal configuration for timeout and interval + - parameter completion: The completion closure used to notify when the server + **/ + public func waitForPendingUpdate( + UID: String, + update: Update, + options: WaitOptions? = nil, + _ completion: @escaping (Result + ) -> Void) { + self.updates.waitForPendingUpdate(UID, update, options, completion) + } + // MARK: Keys /** @@ -867,5 +887,4 @@ public struct MeiliSearch { _ completion: @escaping (Result) -> Void) { self.dumps.status(UID, completion) } - } diff --git a/Sources/MeiliSearch/Error.swift b/Sources/MeiliSearch/Error.swift index 90e5f4ed..ea033989 100644 --- a/Sources/MeiliSearch/Error.swift +++ b/Sources/MeiliSearch/Error.swift @@ -68,6 +68,9 @@ public extension MeiliSearch { /// The input or output JSON is invalid. case invalidJSON + // TimeOut is reached in a waiting function + case timeOut(timeOut: Double) + // URL is invalid case invalidURL(url: String? = "") @@ -88,6 +91,8 @@ public extension MeiliSearch { return "Response decoding failed" case .invalidJSON: return "Invalid json" + case .timeOut(let timeOut): + return "TimeOut of \(timeOut) is reached" case .invalidURL(let url): if let strUrl: String = url { return "Invalid URL: \(strUrl)" diff --git a/Sources/MeiliSearch/Model/Update.swift b/Sources/MeiliSearch/Model/Update.swift index 800b9d9e..60b1ed74 100644 --- a/Sources/MeiliSearch/Model/Update.swift +++ b/Sources/MeiliSearch/Model/Update.swift @@ -11,6 +11,10 @@ public struct Update: Codable, Equatable { /// The UID of the update. public let updateId: Int + public init(updateId: Int) { + self.updateId = updateId + } + /// Result type for the Update. public struct Result: Codable, Equatable { diff --git a/Sources/MeiliSearch/Model/WaitOptions.swift b/Sources/MeiliSearch/Model/WaitOptions.swift new file mode 100644 index 00000000..f4b24289 --- /dev/null +++ b/Sources/MeiliSearch/Model/WaitOptions.swift @@ -0,0 +1,32 @@ +import Foundation + +/** + `WaitOptions` struct represent the options used during a waitForPendingUpdate call. + */ +public struct WaitOptions: Codable, Equatable { + + // MARK: Properties + + /// Maximum time in seconds before timeOut + public let timeOut: Double + + /// Interval in seconds between each status call + public let interval: TimeInterval + + // MARK: Initializers + public init( + timeOut: Double? = 5, + interval: TimeInterval? = 0.5 + ) { + self.timeOut = timeOut ?? 5 + self.interval = interval ?? 0.5 + } + + // MARK: Codable Keys + + enum CodingKeys: String, CodingKey { + case timeOut + case interval + } + +} diff --git a/Sources/MeiliSearch/Updates.swift b/Sources/MeiliSearch/Updates.swift index d9636cfc..2e14e6da 100644 --- a/Sources/MeiliSearch/Updates.swift +++ b/Sources/MeiliSearch/Updates.swift @@ -19,17 +19,13 @@ struct Updates { _ UID: String, _ update: Update, _ completion: @escaping (Result) -> Void) { - self.request.get(api: "/indexes/\(UID)/updates/\(update.updateId)") { result in - switch result { case .success(let data): - guard let data: Data = data else { completion(.failure(MeiliSearch.Error.dataNotFound)) return } - do { let result: Update.Result = try Constants.customJSONDecoder.decode( Update.Result.self, @@ -42,9 +38,7 @@ struct Updates { case .failure(let error): completion(.failure(error)) } - } - } func getAll( @@ -74,7 +68,48 @@ struct Updates { } } + } + + private func checkStatus( + _ UID: String, + _ update: Update, + _ options: WaitOptions, + _ startingDate: Date, + _ completion: @escaping (Result) -> Void) { + self.get(UID, update) { result in + switch result { + case .success(let status): + if status.status == Update.Status.processed || status.status == Update.Status.failed { + completion(.success(status)) + } else if 0 - startingDate.timeIntervalSinceNow > options.timeOut { + completion(.failure(MeiliSearch.Error.timeOut(timeOut: options.timeOut))) + } else { + usleep(useconds_t(options.interval * 1000000)) + self.checkStatus(UID, update, options, startingDate, completion) + } + case .failure(let error): + completion(.failure(error)) + return + } + } + } + func waitForPendingUpdate( + _ UID: String, + _ update: Update, + _ options: WaitOptions? = nil, + _ completion: @escaping (Result) -> Void) { + let currentDate = Date() + let waitOptions: WaitOptions = options ?? WaitOptions() + + self.checkStatus(UID, update, waitOptions, currentDate) { result in + switch result { + case .success(let status): + completion(.success(status)) + case .failure(let error): + completion(.failure(error)) + } + } } } diff --git a/Tests/MeiliSearchIntegrationTests/UpdatesTests.swift b/Tests/MeiliSearchIntegrationTests/UpdatesTests.swift index 2aadee06..e0b39527 100644 --- a/Tests/MeiliSearchIntegrationTests/UpdatesTests.swift +++ b/Tests/MeiliSearchIntegrationTests/UpdatesTests.swift @@ -23,6 +23,17 @@ private struct Movie: Codable, Equatable { } +private let movies: [Movie] = [ + Movie(id: 123, title: "Pride and Prejudice", comment: "A great book"), + Movie(id: 456, title: "Le Petit Prince", comment: "A french book"), + Movie(id: 2, title: "Le Rouge et le Noir", comment: "Another french book"), + Movie(id: 1, title: "Alice In Wonderland", comment: "A weird book"), + Movie(id: 1344, title: "The Hobbit", comment: "An awesome book"), + Movie(id: 4, title: "Harry Potter and the Half-Blood Prince", comment: "The best book"), + Movie(id: 42, title: "The Hitchhiker's Guide to the Galaxy"), + Movie(id: 1844, title: "A Moreninha", comment: "A Book from Joaquim Manuel de Macedo") +] + class UpdatesTests: XCTestCase { private var client: MeiliSearch! @@ -62,12 +73,9 @@ class UpdatesTests: XCTestCase { let documents: Data = try! JSONEncoder().encode([movie]) self.client.addDocuments(UID: self.uid, documents: documents, primaryKey: nil) { result in - switch result { case .success(let update): - self.client.getUpdate(UID: self.uid, update) { (result: Result) in - switch result { case .success(let update): XCTAssertEqual("DocumentsAddition", update.type.name) @@ -76,9 +84,7 @@ class UpdatesTests: XCTestCase { XCTFail() } expectation.fulfill() - } - case .failure: XCTFail("Failed to update movie index") } @@ -101,7 +107,6 @@ class UpdatesTests: XCTestCase { } self.client.getAllUpdates(UID: self.uid) { (result: Result<[Update.Result], Swift.Error>) in - switch result { case .success(let updates): updates.forEach { (update: Update.Result) in @@ -115,11 +120,154 @@ class UpdatesTests: XCTestCase { expectation.fulfill() } + self.wait(for: [expectation], timeout: 10.0) + + } + + func testWaitForPendingUpdateSuccessDefault () { + let expectation = XCTestExpectation(description: "Wait for pending update with default options") + self.client.addDocuments( + UID: self.uid, + documents: movies, + primaryKey: nil + ) { result in + + switch result { + case .success(let update): + XCTAssertEqual(Update(updateId: 0), update) + self.client.waitForPendingUpdate(UID: self.uid, update: update) { result in + switch result { + case .success(let update): + XCTAssertEqual(update.status, Update.Status.processed) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + } self.wait(for: [expectation], timeout: 10.0) + } + + func testWaitForPendingUpdateSuccessEmptyOptions () { + let expectation = XCTestExpectation(description: "Wait for pending update with default options") + + self.client.addDocuments( + UID: self.uid, + documents: movies, + primaryKey: nil + ) { result in + switch result { + case .success(let update): + XCTAssertEqual(Update(updateId: 0), update) + self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions()) { result in + switch result { + case .success(let update): + XCTAssertEqual(update.status, Update.Status.processed) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + case .failure(let error): + print(error) + XCTFail() + } + } + self.wait(for: [expectation], timeout: 5.0) } + func testWaitForPendingUpdateSuccessWithOptions () { + let expectation = XCTestExpectation(description: "Wait for pending update with default options") + + self.client.addDocuments( + UID: self.uid, + documents: movies, + primaryKey: nil + ) { result in + + switch result { + case .success(let update): + XCTAssertEqual(Update(updateId: 0), update) + self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions(timeOut: 5, interval: 2)) { result in + switch result { + case .success(let update): + XCTAssertEqual(update.status, Update.Status.processed) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + self.wait(for: [expectation], timeout: 5.0) + } + + func testWaitForPendingUpdateSuccessWithIntervalZero () { + let expectation = XCTestExpectation(description: "Wait for pending update with default options") + self.client.addDocuments( + UID: self.uid, + documents: movies, + primaryKey: nil + ) { result in + switch result { + case .success(let update): + XCTAssertEqual(Update(updateId: 0), update) + self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions(timeOut: 5, interval: 0)) { result in + switch result { + case .success(let update): + XCTAssertEqual(update.status, Update.Status.processed) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation.fulfill() + } + case .failure(let error): + XCTFail(error.localizedDescription) + } + } + self.wait(for: [expectation], timeout: 5.0) + } + + func testWaitForPendingUpdateTimeOut () { + let expectation = XCTestExpectation(description: "Wait for pending update with default options") + + self.client.addDocuments( + UID: self.uid, + documents: movies, + primaryKey: nil + ) { result in + switch result { + case .success(let update): + XCTAssertEqual(Update(updateId: 0), update) + self.client.waitForPendingUpdate(UID: self.uid, update: update, options: WaitOptions(timeOut: 0, interval: 2)) { result in + switch result { + case .success: + XCTFail("waitForPendingUpdate should not have had the time for a second call") + case .failure(let error): + print(error.localizedDescription) + switch error { + case MeiliSearch.Error.timeOut(let double): + XCTAssertEqual(double, 0.0) + default: + XCTFail("MeiliSearch TimeOut error should have been thrown") + } + } + expectation.fulfill() + } + case .failure(let error): + print(error) + XCTFail() + } + } + self.wait(for: [expectation], timeout: 5.0) + } } // swiftlint:enable force_unwrapping // swiftlint:enable force_try