From 3f7276e92e22a2b0f6778f980db6891c16bc4173 Mon Sep 17 00:00:00 2001 From: Mouaad Aallam Date: Tue, 26 Sep 2023 10:10:19 +0200 Subject: [PATCH] feat(dart): add wait task methods (#2037) --- .../lib/src/algolia_exception.dart | 15 ++ .../client_search/lib/src/extension.dart | 28 +-- .../lib/src/extension/search.dart | 26 +++ .../lib/src/extension/wait_task.dart | 159 ++++++++++++++++++ 4 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/search.dart create mode 100644 clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/wait_task.dart diff --git a/clients/algoliasearch-client-dart/packages/client_core/lib/src/algolia_exception.dart b/clients/algoliasearch-client-dart/packages/client_core/lib/src/algolia_exception.dart index fb9eb3a89f..955bbdd7ea 100644 --- a/clients/algoliasearch-client-dart/packages/client_core/lib/src/algolia_exception.dart +++ b/clients/algoliasearch-client-dart/packages/client_core/lib/src/algolia_exception.dart @@ -52,6 +52,21 @@ final class AlgoliaIOException implements AlgoliaException { } } +/// Exception thrown when an error occurs during the wait strategy. +/// For example: maximum number of retry exceeded. +final class AlgoliaWaitException implements AlgoliaException { + /// The error message. + final dynamic error; + + /// Constructs an [AlgoliaWaitException] with the provided error message. + const AlgoliaWaitException(this.error); + + @override + String toString() { + return 'AlgoliaWaitException{error: $error}'; + } +} + /// Exception thrown when all hosts for the Algolia API are unreachable. /// /// Contains a list of the errors associated with each unreachable host. diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart index 6ffc307a9a..8858090b4c 100644 --- a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension.dart @@ -1,26 +1,2 @@ -import 'package:algolia_client_search/algolia_client_search.dart'; - -extension SearchClientExt on SearchClient { - /// Perform a search operation targeting one index. - Future searchIndex({ - required SearchForHits request, - RequestOptions? requestOptions, - }) async { - final response = await search( - searchMethodParams: SearchMethodParams(requests: [request]), - requestOptions: requestOptions, - ); - return SearchResponse.fromJson(response.results.first); - } - - /// Perform a search operation targeting one index. - Future> searchMultiIndex({ - required List queries, - SearchStrategy? strategy, - RequestOptions? requestOptions, - }) { - final request = SearchMethodParams(requests: queries, strategy: strategy); - return search(searchMethodParams: request, requestOptions: requestOptions) - .then((res) => res.results.map((e) => SearchResponse.fromJson(e))); - } -} +export 'extension/search.dart'; +export 'extension/wait_task.dart'; diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/search.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/search.dart new file mode 100644 index 0000000000..6ffc307a9a --- /dev/null +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/search.dart @@ -0,0 +1,26 @@ +import 'package:algolia_client_search/algolia_client_search.dart'; + +extension SearchClientExt on SearchClient { + /// Perform a search operation targeting one index. + Future searchIndex({ + required SearchForHits request, + RequestOptions? requestOptions, + }) async { + final response = await search( + searchMethodParams: SearchMethodParams(requests: [request]), + requestOptions: requestOptions, + ); + return SearchResponse.fromJson(response.results.first); + } + + /// Perform a search operation targeting one index. + Future> searchMultiIndex({ + required List queries, + SearchStrategy? strategy, + RequestOptions? requestOptions, + }) { + final request = SearchMethodParams(requests: queries, strategy: strategy); + return search(searchMethodParams: request, requestOptions: requestOptions) + .then((res) => res.results.map((e) => SearchResponse.fromJson(e))); + } +} diff --git a/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/wait_task.dart b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/wait_task.dart new file mode 100644 index 0000000000..76bc233b00 --- /dev/null +++ b/clients/algoliasearch-client-dart/packages/client_search/lib/src/extension/wait_task.dart @@ -0,0 +1,159 @@ +import 'package:algolia_client_core/algolia_client_core.dart'; +import 'package:algolia_client_search/src/api/search_client.dart'; +import 'package:algolia_client_search/src/model/api_key.dart'; +import 'package:algolia_client_search/src/model/get_api_key_response.dart'; +import 'package:algolia_client_search/src/model/task_status.dart'; +import 'package:collection/collection.dart'; + +extension WaitTask on SearchClient { + /// Wait for a [taskID] to complete before executing the next line of code, to synchronize index + /// updates. All write operations in Algolia are asynchronous by design. It means that when you add + /// or update an object to your index, our servers will reply to your request with a [taskID] as soon + /// as they understood the write operation. The actual insert and indexing will be done after + /// replying to your code. You can wait for a task to complete by using the [taskID] and this method. + Future waitTask({ + required String indexName, + required int taskID, + WaitParams params = const WaitParams(), + RequestOptions? requestOptions, + }) async { + await _waitUntil( + params: params, + retry: () => getTask( + indexName: indexName, + taskID: taskID, + requestOptions: requestOptions, + ), + until: (response) => response.status == TaskStatus.published, + ); + } + + /// Wait on an API key creation operation. + Future waitKeyCreation({ + required String key, + int maxRetries = 50, + WaitParams params = const WaitParams(), + RequestOptions? requestOptions, + }) async { + await _waitUntil( + retry: () async { + try { + return await getApiKey(key: key, requestOptions: requestOptions); + } on AlgoliaApiException catch (_) { + return null; + } + }, + until: (result) => result != null, + params: params, + ); + } + + /// Wait on a delete API ket operation. + Future waitKeyDeletion({ + required String key, + WaitParams params = const WaitParams(), + RequestOptions? requestOptions, + }) async { + await _waitUntil( + params: params, + retry: () async { + try { + return await getApiKey(key: key, requestOptions: requestOptions); + } on AlgoliaApiException catch (e) { + return e; + } + }, + until: (result) => + result is AlgoliaApiException ? result.statusCode == 404 : false, + ); + } + + /// Wait on an API key update operation. + Future waitKeyUpdate({ + required String key, + required ApiKey apiKey, + WaitParams params = const WaitParams(), + RequestOptions? requestOptions, + }) async { + await _waitUntil( + params: params, + retry: () async => + await getApiKey(key: key, requestOptions: requestOptions), + until: (response) => _isExpectedApiKey(apiKey, response), + ); + } +} + +/// Wait operation parameters. +class WaitParams { + final int maxRetries; + final Duration? timeout; + final Duration initialDelay; + final Duration maxDelay; + + const WaitParams({ + this.maxRetries = 50, + this.timeout, + this.initialDelay = const Duration(milliseconds: 200), + this.maxDelay = const Duration(seconds: 5), + }); +} + +/// Checks if [response] contains the expected updates in [apiKey]. +bool _isExpectedApiKey(ApiKey apiKey, GetApiKeyResponse response) { + return const DeepCollectionEquality.unordered() + .equals(apiKey.acl, response.acl) && + (apiKey.description == null || + (apiKey.description != null && + apiKey.description == response.description)) && + (apiKey.indexes == null || + (apiKey.indexes != null && + const DeepCollectionEquality.unordered() + .equals(apiKey.indexes, response.indexes))) && + (apiKey.maxHitsPerQuery == null || + (apiKey.maxHitsPerQuery != null && + apiKey.maxHitsPerQuery == response.maxHitsPerQuery)) && + (apiKey.maxQueriesPerIPPerHour == null || + (apiKey.maxQueriesPerIPPerHour != null && + apiKey.maxQueriesPerIPPerHour == + response.maxQueriesPerIPPerHour)) && + (apiKey.queryParameters == null || + (apiKey.queryParameters != null && + apiKey.queryParameters == response.queryParameters)) && + (apiKey.referers == null || + (apiKey.referers != null && + const DeepCollectionEquality.unordered() + .equals(apiKey.referers, response.referers))) && + (apiKey.validity == null || + (apiKey.validity != null && + apiKey.validity == response.validity)); +} + +/// Retries the given [retry] function until the [until] condition is satisfied or the maximum number +/// of [maxRetries] or [timeout] is reached. +Future _waitUntil({ + required Future Function() retry, + required bool Function(T) until, + required WaitParams params, +}) async { + Future wait() async { + var currentDelay = params.initialDelay; + for (var i = 0; i < params.maxRetries; i++) { + var result = await retry(); + if (until(result)) return result; + await Future.delayed(currentDelay); + var newDelay = currentDelay * 2; + currentDelay = newDelay < params.maxDelay ? newDelay : params.maxDelay; + } + throw AlgoliaWaitException( + "The maximum number of retries ($params.maxRetries) exceeded"); + } + + final timeout = params.timeout; + return timeout == null + ? wait() + : wait().timeout( + timeout, + onTimeout: () => throw Exception("Timeout of $timeout ms exceeded"), + ); +}