diff --git a/example/lib/api/application_api.dart b/example/lib/api/application_api.dart index c76eb6c..447620b 100644 --- a/example/lib/api/application_api.dart +++ b/example/lib/api/application_api.dart @@ -12,12 +12,17 @@ class ApplicationApi extends ApiClient { dio: dio, ); - Future getUserList({int? page}) => get( + Future getUserList({ + int? page, + CancelToken? cancelToken, + }) => + get( path: 'users${page != null ? '?page=$page' : ''}', responseMapper: response_mappers.users, validate: false, receiveTimeout: Duration(seconds: 30), sendTimeout: Duration(seconds: 30), + cancelToken: cancelToken, ); Future saveAuthTokens(LoginResponseModel response) { @@ -38,4 +43,13 @@ class ApplicationApi extends ApiClient { receiveTimeout: Duration(seconds: 30), sendTimeout: Duration(seconds: 30), ); + + Future login() => post( + path: '/register', + body: {"email": "eve.holt@reqres.in", "password": "cityslicka"}, + responseMapper: (res) => res, + validate: false, + receiveTimeout: Duration(seconds: 30), + sendTimeout: Duration(seconds: 30), + ); } diff --git a/example/lib/app.dart b/example/lib/app.dart index c284677..e176cd5 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:developer'; +import 'package:dash_kit_network/dash_kit_network.dart'; import 'package:example/api/application_api.dart'; import 'package:example/api/models/user_response_model.dart'; import 'package:flutter/material.dart'; @@ -174,6 +176,7 @@ class _MyHomePageState extends State { try { final response = await widget.apiClient.getUserList(); users = response.data; + currentPage = 1; } catch (e) { showErrorDialog(); } @@ -188,7 +191,17 @@ class _MyHomePageState extends State { try { currentPage++; - final response = await widget.apiClient.getUserList(page: currentPage); + log('currentPage: $currentPage', name: 'currentPage'); + final cancelToken = CancelToken(); + if (currentPage == 3) { + Future.delayed(const Duration(milliseconds: 100), () { + cancelToken.cancel(); + }); + } + final response = await widget.apiClient.getUserList( + page: currentPage, + cancelToken: cancelToken, + ); users.addAll(response.data); } catch (e) { showErrorDialog(); diff --git a/example/pubspec.lock b/example/pubspec.lock index 1b99ed1..ef05e6b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -347,10 +347,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -379,10 +379,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -395,10 +395,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -419,10 +419,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider_linux: dependency: transitive description: @@ -648,10 +648,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -717,5 +717,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=3.0.0-0 <4.0.0" flutter: ">=3.0.0" diff --git a/lib/src/api_client.dart b/lib/src/api_client.dart index 412299c..05df476 100644 --- a/lib/src/api_client.dart +++ b/lib/src/api_client.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:dash_kit_network/src/error_handler_delegate.dart'; @@ -26,6 +27,7 @@ abstract class ApiClient { this.delegate, this.commonHeaders = const [], this.errorHandlerDelegate, + IsolateManager? externalIsolateManager, }) : _tokenManager = delegate == null ? null : TokenManager( @@ -37,17 +39,18 @@ abstract class ApiClient { return newTokenPair; }, - ) { + ), _isolateManager = externalIsolateManager ?? IsolateManager() { dio.options.baseUrl = environment.baseUrl; + _isolateManager.start(); } - final isolateManager = IsolateManager()..start(); final ApiEnvironment environment; final Dio dio; final List commonHeaders; final ErrorHandlerDelegate? errorHandlerDelegate; final RefreshTokensDelegate? delegate; final TokenManager? _tokenManager; + final IsolateManager _isolateManager; Future get({ required String path, @@ -227,20 +230,11 @@ abstract class ApiClient { throw const RefreshTokensDelegateMissingException(); } - final performRequest = (tokenPair) async { - final response = await _createRequest(params, tokenPair); - - return await isolateManager.sendTask( - response: response, - mapper: params.responseMapper, - ); - }; - if (params.isAuthorisedRequest) { try { final tokens = await _tokenManager!.getTokens(); - return await performRequest(tokens); + return await _createRequest(params, tokens); } catch (error) { if (error is DioError && (delegate?.isAccessTokenExpired(error) ?? false)) { @@ -253,7 +247,7 @@ abstract class ApiClient { return Error.throwWithStackTrace(refreshError, st); }); - return performRequest(refreshedTokens); + return _createRequest(params, refreshedTokens); } if (error is DioError && @@ -265,7 +259,7 @@ abstract class ApiClient { } } - return performRequest(null); + return _createRequest(params, null); } Map _headers(List headers) => @@ -276,11 +270,10 @@ abstract class ApiClient { }); // ignore: long-method - Future> _createRequest( + Future _createRequest( RequestParams params, TokenPair? tokenPair, ) async { - final cancelToken = params.cancelToken; var options = Options( headers: _headers([...params.headers, ...commonHeaders]), responseType: params.responseType, @@ -301,8 +294,7 @@ abstract class ApiClient { return await _createDioRequest( params, options, - cancelToken, - ); + ) as T; } catch (error, stackTrace) { if (error is DioError) { final response = error.response; @@ -313,7 +305,7 @@ abstract class ApiClient { (errorHandlerDelegate?.canHandleError(error) ?? false))) { rethrow; } else if (!params.validate && response != null) { - return Future.value(response); + return Future.value(await params.responseMapper(response)); } else if (_isNetworkConnectionError(type, error)) { return Error.throwWithStackTrace( NetworkConnectionException(error), @@ -336,56 +328,15 @@ abstract class ApiClient { } } - Future> _createDioRequest( + Future> _createDioRequest( RequestParams params, Options options, - CancelToken? cancelToken, ) { - switch (params.method) { - case HttpMethod.get: - return dio.get( - params.path, - queryParameters: params.queryParams, - options: options, - cancelToken: cancelToken, - ); - - case HttpMethod.post: - return dio.post( - params.path, - data: params.body, - queryParameters: params.queryParams, - options: options, - cancelToken: cancelToken, - ); - - case HttpMethod.put: - return dio.put( - params.path, - data: params.body, - queryParameters: params.queryParams, - options: options, - cancelToken: cancelToken, - ); - - case HttpMethod.patch: - return dio.patch( - params.path, - data: params.body, - queryParameters: params.queryParams, - options: options, - cancelToken: cancelToken, - ); - - case HttpMethod.delete: - return dio.delete( - params.path, - data: params.body, - queryParameters: params.queryParams, - options: options, - cancelToken: cancelToken, - ); - } + return _isolateManager.sendTask( + params: params, + options: options, + dioMethod: params.method.convertToDio(dio), + ); } bool _isNetworkConnectionError(DioErrorType type, DioError error) { @@ -410,3 +361,26 @@ abstract class ApiClient { } enum HttpMethod { get, post, put, patch, delete } + +extension HttpMethodExtension on HttpMethod { + Future> Function( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) convertToDio(Dio dio) { + switch (this) { + case HttpMethod.get: + return dio.get; + case HttpMethod.post: + return dio.post; + case HttpMethod.put: + return dio.put; + case HttpMethod.patch: + return dio.patch; + case HttpMethod.delete: + return dio.delete; + } + } +} diff --git a/lib/src/isolate_manager/isolate_manager_interface.dart b/lib/src/isolate_manager/isolate_manager_interface.dart index 308b34a..aa70d64 100644 --- a/lib/src/isolate_manager/isolate_manager_interface.dart +++ b/lib/src/isolate_manager/isolate_manager_interface.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:dash_kit_network/dash_kit_network.dart'; +import 'package:dash_kit_network/src/models/request_params.dart'; import 'package:flutter/foundation.dart'; /// This is an abstract class for managing isolates. @@ -15,7 +16,6 @@ abstract class IsolateManagerInterface { /// /// Should be called before sending messages. If `start()` is called multiple /// times, an assertion error is thrown. - @protected @mustCallSuper Future start() async { assert(_isolateStatus == IsolateStatus.created, 'Isolate already started'); @@ -28,7 +28,6 @@ abstract class IsolateManagerInterface { /// /// Should be called when the isolate is no longer needed. /// Throws an assertion error if the isolate is not started. - @protected @mustCallSuper void stop() { assert( @@ -40,24 +39,32 @@ abstract class IsolateManagerInterface { /// Method for sending a [Task] to the isolate. /// - /// This method get a `response` and a `mapper` as parameters. - /// The response [Response] is a response from the network request. - /// The mapper [FutureOr Function(Response)] is a function that - /// takes a [Response] and returns a [FutureOr]. + /// This method get the [RequestParams], [Options] and the [Dio] method and + /// creates a [Task] object. The [Task] object is sent to the isolate and the + /// [ResponseMapper] function is executed in the isolate. /// /// ```dart /// IsolateManager isolateManager = IsolateManager(); /// await isolateManager.start(); - /// final result = await isolateManager.send( - /// response: response, - /// mapper: responseMapper, + /// final result = await isolateManager.sendTask( + /// params: requestParams, + /// options: options, + /// dioMethod: dio.get, + /// cancelToken: cancelToken, /// ); /// ``` - @protected @mustCallSuper Future sendTask({ - required Response response, - required FutureOr Function(Response) mapper, + required RequestParams params, + required Options options, + required Future> Function( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) + dioMethod, }) { assert( _isolateStatus == IsolateStatus.initialized || @@ -77,25 +84,54 @@ abstract class IsolateManagerInterface { } } -/// The [Task] class is used to store the data needed to execute the mapper +abstract class Task { + const Task({required this.id}); + + final String id; +} + +/// The [WorkTask] class is used to store the data needed to execute the mapper /// function in the isolate. -class Task { - const Task( - this.id, - this.response, - this.mapper, - ); +class WorkTask implements Task { + const WorkTask({ + required this.id, + required this.params, + required this.options, + required this.dioMethod, + required this.needCancelToken, + }); + @override final String id; - final Response response; - final T Function(Response) mapper; + final RequestParams params; + final Options options; + final Future> Function( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) dioMethod; + final bool needCancelToken; @override String toString() { - return 'Task{id: $id, response: $response, mapper: $mapper}'; + return 'Task{id: $id, params: $params, options: $options, ' + 'dioMethod: $dioMethod, cancelToken: $needCancelToken}'; } } +class CancelTask implements Task { + CancelTask({ + required this.id, + required this.error, + }); + + @override + final String id; + final DioError error; +} + /// The [Result] class is used to store the result of the mapper function /// execution in the isolate. class Result { diff --git a/lib/src/isolate_manager/isolate_manager_io/isolate_manager.dart b/lib/src/isolate_manager/isolate_manager_io/isolate_manager.dart index 40631f6..7a5e694 100644 --- a/lib/src/isolate_manager/isolate_manager_io/isolate_manager.dart +++ b/lib/src/isolate_manager/isolate_manager_io/isolate_manager.dart @@ -4,6 +4,8 @@ import 'dart:isolate'; import 'package:dash_kit_network/dash_kit_network.dart'; import 'package:dash_kit_network/src/isolate_manager/isolate_manager_interface.dart'; +import 'package:dash_kit_network/src/models/request_params.dart'; +import 'package:flutter/foundation.dart'; /// The `IsolateManager` class manages a separate isolate for executing tasks /// asynchronously. It implements the `IsolateManagerInterface` interface, @@ -65,33 +67,75 @@ class IsolateManager extends IsolateManagerInterface { /// /// Waits for _managerReadyCompleter to complete before sending the task. /// Throws an error if the result contains an error. + /// /// ```dart /// IsolateManager isolateManager = IsolateManager(); /// await isolateManager.start(); - /// final result = await isolateManager.send( - /// response: response, - /// mapper: responseMapper, + /// final result = await isolateManager.sendTask( + /// params: requestParams, + /// options: options, + /// dioMethod: dio.get, + /// cancelToken: cancelToken, /// ); /// ``` @override Future> sendTask({ - required Response response, - required FutureOr Function(Response) mapper, + required RequestParams params, + required Options options, + required Future> Function( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) + dioMethod, }) async { await _managerReadyCompleter.future; - await super.sendTask(response: response, mapper: mapper); - final task = Task(response.hashCode.toString(), response, mapper); - - _sendPort.send(task); + await super.sendTask( + params: params, + options: options, + dioMethod: dioMethod, + ); + final completer = Completer(); + final task = WorkTask( + id: UniqueKey().toString(), + params: params.isolateInstance, + options: options, + dioMethod: dioMethod, + needCancelToken: params.cancelToken != null, + ); - final result = - await _resultStream.where((event) => event.id == task.id).first; + // ignore: unawaited_futures + params.cancelToken?.whenCancel.then((err) { + completer.completeError(err); + final cancelTask = CancelTask(id: task.id, error: err); + _sendPort.send(cancelTask); + log('Request was canceled: $err', name: 'IsolateManager'); + }); - if (result.error != null) { - throw result.error!; + try { + _sendPort.send(task); + } catch (e, stackTrace) { + log('$e,\n $stackTrace', name: 'SendPort.send'); + completer.completeError(e); } - return result.result; + unawaited(_resultStream + .where((event) => event.id == task.id) + .first + .then((result) { + if (result.error != null) { + if (completer.isCompleted) { + return; + } + completer.completeError(result.error!); + } else { + completer.complete(result.result); + } + })); + + return completer.future; } /// Handles incoming messages from the isolate. @@ -110,24 +154,45 @@ class IsolateManager extends IsolateManagerInterface { /// /// We shouldn't store and cancel subscription, because the isolate will be /// killed after `IsolateManager.stop` is called. +// ignore: long-method void _isolateFunction(SendPort sendPort) { final receivePort = ReceivePort(); sendPort.send(receivePort.sendPort); + final cancelTokensMap = {}; receivePort.listen( (message) async { assert(message is Task, 'Message should be, a Task'); - try { - final result = await message.mapper(message.response); - sendPort.send(Result(id: message.id, result: result)); - } catch (e, stackTrace) { - log('Error: $e'); - log('$stackTrace'); - sendPort.send(Result( - id: message.id, - error: e, - )); + if (message is WorkTask) { + if (message.needCancelToken) { + cancelTokensMap[message.id] = CancelToken(); + } + try { + final response = await message.dioMethod( + message.params.path, + data: message.params.body, + queryParameters: message.params.queryParams, + options: message.options, + cancelToken: cancelTokensMap[message.id], + ); + final result = await message.params.responseMapper(response); + sendPort.send(Result(id: message.id, result: result)); + } catch (e, stackTrace) { + log('Error: $e', name: 'isolateFunction'); + log('$stackTrace', name: 'isolateFunction'); + sendPort.send(Result( + id: message.id, + error: e, + )); + } + } + if (message is CancelTask) { + final cancelToken = cancelTokensMap.remove(message.id); + if (cancelToken != null) { + cancelToken.cancel(); + } } }, + cancelOnError: false, ); } diff --git a/lib/src/isolate_manager/isolate_manager_web/isolate_manager.dart b/lib/src/isolate_manager/isolate_manager_web/isolate_manager.dart index 59d700d..b62154a 100644 --- a/lib/src/isolate_manager/isolate_manager_web/isolate_manager.dart +++ b/lib/src/isolate_manager/isolate_manager_web/isolate_manager.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:dash_kit_network/dash_kit_network.dart'; import 'package:dash_kit_network/src/isolate_manager/isolate_manager_interface.dart'; +import 'package:dash_kit_network/src/models/request_params.dart'; /// The IsolateManager class implements the IsolateManagerInterface. /// @@ -20,22 +21,49 @@ class IsolateManager extends IsolateManagerInterface { /// Implementation for web environment where isolates aren't available. /// - /// Instead the `send` method executes the mapper function and returns result. + /// Instead the `dioMethod` is called directly. Is waited response,is called + /// `responseMapper` and is returned `responseMapper` result. + /// /// ```dart /// IsolateManager isolateManager = IsolateManager(); /// await isolateManager.start(); - /// final result = await isolateManager.send( - /// response: response, - /// mapper: responseMapper, + /// final result = await isolateManager.sendTask( + /// params: requestParams, + /// options: options, + /// dioMethod: dio.get, + /// cancelToken: cancelToken, /// ); /// ``` @override Future> sendTask({ - required Response response, - required FutureOr Function(Response) mapper, + required RequestParams params, + required Options options, + required Future> Function( + String path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) + dioMethod, }) async { - await super.sendTask(response: response, mapper: mapper); + await super + .sendTask(params: params, options: options, dioMethod: dioMethod); + final completer = Completer(); + + try { + final response = await dioMethod( + params.path, + data: params.body, + queryParameters: params.queryParams, + options: options, + cancelToken: params.cancelToken, + ); + completer.complete(await params.responseMapper(response)); + } catch (e) { + completer.completeError(e); + } - return mapper(response); + return completer.future; } } diff --git a/lib/src/models/request_params.dart b/lib/src/models/request_params.dart index 9e995a3..6593e5d 100644 --- a/lib/src/models/request_params.dart +++ b/lib/src/models/request_params.dart @@ -33,4 +33,22 @@ class RequestParams { final Duration? sendTimeout; final String contentType; final CancelToken? cancelToken; + + /// Returns a copy of this [RequestParams] with the fields without cancelToken + /// [CancelToken], because it is not serializable and cannot be sent to + /// the isolate. + RequestParams get isolateInstance => RequestParams( + method: this.method, + path: this.path, + headers: this.headers, + responseMapper: this.responseMapper, + isAuthorisedRequest: this.isAuthorisedRequest, + validate: this.validate, + body: this.body, + queryParams: this.queryParams, + responseType: this.responseType, + receiveTimeout: this.receiveTimeout, + sendTimeout: this.sendTimeout, + contentType: this.contentType, + ); } diff --git a/pubspec.lock b/pubspec.lock index 2558f77..4c344d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: dart_code_metrics - sha256: bb4ec5e729788dde5f7e8e9df4c05ec3b78532a5763e635337153ce40085514b + sha256: "8d40e9fa6ba6b5f4f569768e267e9cb3e74350f98b30ac4b8160795792ce5414" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.7.4" dart_code_metrics_presets: dependency: transitive description: @@ -221,10 +221,10 @@ packages: dependency: "direct dev" description: name: dash_kit_lints - sha256: "0c6cab89b403c977136174f0364e4f15bc0a3a770c3f470e0340040ce2841b03" + sha256: "2978f8a2b87c664a81a892283a98dd5e30e7ab6af1047dc12be74ed40fc90b1f" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.1" dio: dependency: "direct main" description: @@ -404,10 +404,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -436,10 +436,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -452,10 +452,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -468,10 +468,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.4.0" node_preamble: dependency: transitive description: @@ -492,10 +492,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider_linux: dependency: transitive description: @@ -572,10 +572,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" + sha256: "05ae70703e06f7fdeb05f7f02dd680b8aad810e87c756a618f33e1794635115c" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "0.3.0" pubspec_parse: dependency: transitive description: @@ -761,26 +761,26 @@ packages: dependency: "direct dev" description: name: test - sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" url: "https://pub.dev" source: hosted - version: "1.22.0" + version: "1.24.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" test_core: dependency: transitive description: name: test_core - sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" url: "https://pub.dev" source: hosted - version: "0.4.20" + version: "0.5.1" timing: dependency: transitive description: @@ -797,6 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -870,5 +878,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index b6fc786..0d57ba1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 3.8.0 homepage: https://github.com/Dash-Kit/dash-kit-network environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.16.0 <3.0.0" dependencies: dio: ^5.1.1 @@ -15,8 +15,8 @@ dependencies: shared_preferences: ^2.1.0 dev_dependencies: - dash_kit_lints: ^1.0.2 build_runner: ^2.3.3 + dash_kit_lints: ^2.0.1 flutter_test: sdk: flutter mockito: ^5.3.2 diff --git a/test/api_client_test.dart b/test/api_client_test.dart index 38186c2..338787e 100644 --- a/test/api_client_test.dart +++ b/test/api_client_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dash_kit_network/dash_kit_network.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/isolate_manager_test.dart b/test/isolate_manager_test.dart index 55d3e58..9f06e1a 100644 --- a/test/isolate_manager_test.dart +++ b/test/isolate_manager_test.dart @@ -1,8 +1,11 @@ import 'package:dash_kit_network/dash_kit_network.dart'; import 'package:dash_kit_network/src/isolate_manager/isolate_manager_io/isolate_manager.dart' if (dart.library.html) 'package:dash_kit_network/src/isolate_manager/isolate_manager_web/isolate_manager.dart'; +import 'package:dash_kit_network/src/models/request_params.dart'; import 'package:flutter_test/flutter_test.dart'; +// ignore_for_file: avoid-unused-parameters + void main() { final response = Response( statusCode: 200, @@ -17,7 +20,7 @@ void main() { ); final asyncMapper = (response) async { - await Future.delayed(Duration.zero); + await Future.delayed(const Duration(seconds: 1)); if (response.statusCode == 200) { return response.data; } @@ -29,41 +32,63 @@ void main() { ); }; - final syncMapper = (response) { - if (response.statusCode == 200) { - return response.data; + final params = RequestParams( + method: HttpMethod.get, + path: '', + headers: [], + responseMapper: asyncMapper, + isAuthorisedRequest: false, + validate: false, + ); + + Future dioMethod( + String? path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + await Future.delayed(const Duration(seconds: 1)); + if (cancelToken?.cancelError != null) { + throw cancelToken!.cancelError!; } - throw DioError.badResponse( - statusCode: response.statusCode, - requestOptions: response.requestOptions, - response: response, - ); - }; + return response; + } + + Future dioMethodError( + String? path, { + Object? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + await Future.delayed(Duration.zero); + + return errorResponse; + } test('Isolate manager send request before start', () { final isolateManager = IsolateManager(); isolateManager - .sendTask(response: response, mapper: syncMapper) + .sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ) .then((value) => expect(value, response.data)); isolateManager.start(); }); - test('Isolate manager async mapper', () async { - final isolateManager = IsolateManager(); - await isolateManager.start(); - final result = await isolateManager.sendTask( - response: response, - mapper: asyncMapper, - ); - expect(result, response.data); - }); - test('Isolate manager error response', () async { final isolateManager = IsolateManager(); await isolateManager.start(); await expectLater( - isolateManager.sendTask(response: errorResponse, mapper: asyncMapper), + isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethodError, + ), throwsA(isA()), ); }); @@ -77,8 +102,11 @@ void main() { test('Isolate after start and stop', () async { final isolateManager = IsolateManager(); await isolateManager.start(); - final result = - await isolateManager.sendTask(response: response, mapper: asyncMapper); + final result = await isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ); expect(result, response.data); isolateManager.stop(); await expectLater(isolateManager.start(), throwsA(isA())); @@ -87,8 +115,11 @@ void main() { test('Isolate stopped twice', () async { final isolateManager = IsolateManager(); await isolateManager.start(); - final result = - await isolateManager.sendTask(response: response, mapper: asyncMapper); + final result = await isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ); expect(result, response.data); isolateManager.stop(); expect(isolateManager.stop, throwsA(isA())); @@ -102,8 +133,11 @@ void main() { test('Isolate stopped', () async { final isolateManager = IsolateManager(); await isolateManager.start(); - final result = - await isolateManager.sendTask(response: response, mapper: asyncMapper); + final result = await isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ); expect(result, response.data); expect(isolateManager.stop, isA()); }); @@ -111,13 +145,52 @@ void main() { test('Isolate send after stopped', () async { final isolateManager = IsolateManager(); await isolateManager.start(); - final result = - await isolateManager.sendTask(response: response, mapper: asyncMapper); + final result = await isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ); expect(result, response.data); isolateManager.stop(); await expectLater( - isolateManager.sendTask(response: response, mapper: syncMapper), + isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ), throwsA(isA()), ); }); + + test('Isolate cancel request', () async { + final isolateManager = IsolateManager(); + final paramsWithCancelToken = RequestParams( + method: HttpMethod.get, + path: '', + headers: [], + responseMapper: asyncMapper, + isAuthorisedRequest: false, + validate: false, + cancelToken: CancelToken(), + ); + + await isolateManager.start(); + final result = await isolateManager.sendTask( + params: params, + options: Options(), + dioMethod: dioMethod, + ); + expect(result, response.data); + Future.delayed(const Duration(milliseconds: 200), () { + paramsWithCancelToken.cancelToken?.cancel(); + }); + await expectLater( + isolateManager.sendTask( + params: paramsWithCancelToken, + options: Options(), + dioMethod: dioMethod, + ), + throwsA(isA()), + ); + }); } diff --git a/test/test_components/test_api_client.dart b/test/test_components/test_api_client.dart index c4695b1..a692b3d 100644 --- a/test/test_components/test_api_client.dart +++ b/test/test_components/test_api_client.dart @@ -1,10 +1,13 @@ import 'package:dash_kit_network/dash_kit_network.dart'; +import 'test_isolate_manager.dart'; + class TestApiClient extends ApiClient { TestApiClient(Dio dio, RefreshTokensDelegate delegate) : super( environment: const ApiEnvironment(baseUrl: 'https://base-url.com'), dio: dio, delegate: delegate, + externalIsolateManager: TestIsolateManager(), ); } diff --git a/test/test_components/test_isolate_manager.dart b/test/test_components/test_isolate_manager.dart new file mode 100644 index 0000000..7b73ecc --- /dev/null +++ b/test/test_components/test_isolate_manager.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:dash_kit_network/dash_kit_network.dart'; +import 'package:dash_kit_network/src/isolate_manager/isolate_manager_io/isolate_manager.dart' + if (dart.library.html) 'package:dash_kit_network/src/isolate_manager/isolate_manager_web/isolate_manager.dart'; +import 'package:dash_kit_network/src/models/request_params.dart'; +import 'package:flutter/foundation.dart'; + +// This is necessary for tests, because we can't test count of executed methods +// in isolate. +class TestIsolateManager extends IsolateManager { + TestIsolateManager() : super(); + + int count = 0; + + @override + Future> sendTask({ + required RequestParams params, + required Options options, + required Future> Function( + String path, { + CancelToken? cancelToken, + Object? data, + Options? options, + Map? queryParameters, + }) + dioMethod, + }) async { + if (!kIsWeb) { + await dioMethod( + params.path, + data: params.body, + queryParameters: params.queryParams, + options: options, + cancelToken: params.cancelToken, + ); + } + + return super.sendTask( + params: params, + options: options, + dioMethod: dioMethod, + ); + } +}