From 83c8191aa311f35ad06db1fecb10453a896c79e0 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 19 Sep 2022 10:33:53 +0100 Subject: [PATCH] Provide an example using an Isolate Worker Pool with Squadron (#361) --- ...son_serializable_squadron_worker_pool.dart | 164 +++++++++++++++ .../lib/json_decode_service.activator.g.dart | 9 + example/lib/json_decode_service.dart | 21 ++ example/lib/json_decode_service.vm.g.dart | 13 ++ example/lib/json_decode_service.worker.g.dart | 59 ++++++ example/pubspec.yaml | 2 + faq.md | 194 ++++++++++++++++++ 7 files changed, 462 insertions(+) create mode 100644 example/bin/main_json_serializable_squadron_worker_pool.dart create mode 100644 example/lib/json_decode_service.activator.g.dart create mode 100644 example/lib/json_decode_service.dart create mode 100644 example/lib/json_decode_service.vm.g.dart create mode 100644 example/lib/json_decode_service.worker.g.dart diff --git a/example/bin/main_json_serializable_squadron_worker_pool.dart b/example/bin/main_json_serializable_squadron_worker_pool.dart new file mode 100644 index 00000000..2c3bbb08 --- /dev/null +++ b/example/bin/main_json_serializable_squadron_worker_pool.dart @@ -0,0 +1,164 @@ +/// This example uses +/// - https://github.com/google/json_serializable.dart +/// - https://github.com/d-markey/squadron +/// - https://github.com/d-markey/squadron_builder + +import 'dart:async' show FutureOr; +import 'dart:convert' show jsonDecode; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/json_decode_service.dart'; +import 'package:chopper_example/json_serializable.dart'; +import 'package:http/testing.dart'; +import 'package:squadron/squadron.dart'; +import 'package:http/http.dart' as http; + +import 'main_json_serializable.dart' show authHeader; + +typedef JsonFactory = T Function(Map json); + +/// This JsonConverter works with or without a WorkerPool +class JsonSerializableWorkerPoolConverter extends JsonConverter { + const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]); + + final Map factories; + final JsonDecodeServiceWorkerPool? workerPool; + + T? _decodeMap(Map values) { + /// Get jsonFactory using Type parameters + /// if not found or invalid, throw error or return null + final jsonFactory = factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + /// throw serializer not found error; + return null; + } + + return jsonFactory(values); + } + + List _decodeList(Iterable values) => + values.where((v) => v != null).map((v) => _decode(v)).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); + + if (entity is Map) return _decodeMap(entity as Map); + + return entity; + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertResponse(response); + + return jsonRes.copyWith(body: _decode(jsonRes.body)); + } + + @override + FutureOr convertError(Response response) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertError(response); + + return jsonRes.copyWith( + body: ResourceError.fromJsonFactory(jsonRes.body), + ); + } + + @override + FutureOr tryDecodeJson(String data) async { + try { + // if there is a worker pool use it, otherwise run in the main thread + return workerPool != null + ? await workerPool!.jsonDecode(data) + : jsonDecode(data); + } catch (error) { + print(error); + + chopperLogger.warning(error); + + return data; + } + } +} + +/// Simple client to have working example without remote server +final client = MockClient((http.Request req) async { + if (req.method == 'POST') { + return http.Response('{"type":"Fatal","message":"fatal error"}', 500); + } + if (req.method == 'GET' && req.headers['test'] == 'list') { + return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + + return http.Response('{"id":"1","name":"Foo"}', 200); +}); + +/// inspired by https://github.com/d-markey/squadron_sample/blob/main/lib/main.dart +void initSquadron(String id) { + Squadron.setId(id); + Squadron.setLogger(ConsoleSquadronLogger()); + Squadron.logLevel = SquadronLogLevel.all; + Squadron.debugMode = true; +} + +Future main() async { + /// initialize Squadron before using it + initSquadron('worker_pool_example'); + + final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool( + // Set whatever you want here + concurrencySettings: ConcurrencySettings.oneCpuThread, + ); + + /// start the Worker Pool + await jsonDecodeServiceWorkerPool.start(); + + final converter = JsonSerializableWorkerPoolConverter( + { + Resource: Resource.fromJsonFactory, + }, + // make sure to provide the WorkerPool to the JsonConverter + jsonDecodeServiceWorkerPool, + ); + + final chopper = ChopperClient( + client: client, + baseUrl: 'http://localhost:8000', + // bind your object factories here + converter: converter, + errorConverter: converter, + services: [ + // the generated service + MyService.create(), + ], + /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ + interceptors: [authHeader], + ); + + final myService = chopper.getService(); + + /// All of the calls below will use jsonDecode in an Isolate worker + final response1 = await myService.getResource('1'); + print('response 1: ${response1.body}'); // undecoded String + + final response2 = await myService.getResources(); + print('response 2: ${response2.body}'); // decoded list of Resources + + final response3 = await myService.getTypedResource(); + print('response 3: ${response3.body}'); // decoded Resource + + final response4 = await myService.getMapResource('1'); + print('response 4: ${response4.body}'); // undecoded Resource + + try { + await myService.newResource(Resource('3', 'Super Name')); + } on Response catch (error) { + print(error.body); + } + + /// stop the Worker Pool + jsonDecodeServiceWorkerPool.stop(); +} diff --git a/example/lib/json_decode_service.activator.g.dart b/example/lib/json_decode_service.activator.g.dart new file mode 100644 index 00000000..1756d3d3 --- /dev/null +++ b/example/lib/json_decode_service.activator.g.dart @@ -0,0 +1,9 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// SquadronWorkerGenerator +// ************************************************************************** + +import 'json_decode_service.vm.g.dart'; + +final $JsonDecodeServiceActivator = $getJsonDecodeServiceActivator(); diff --git a/example/lib/json_decode_service.dart b/example/lib/json_decode_service.dart new file mode 100644 index 00000000..41c94785 --- /dev/null +++ b/example/lib/json_decode_service.dart @@ -0,0 +1,21 @@ +/// This example uses https://github.com/d-markey/squadron_builder + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:squadron/squadron.dart'; +import 'package:squadron/squadron_annotations.dart'; + +import 'json_decode_service.activator.g.dart'; + +part 'json_decode_service.worker.g.dart'; + +@SquadronService( + // disable web to keep the number of generated files low for this example + web: false, +) +class JsonDecodeService extends WorkerService + with $JsonDecodeServiceOperations { + @SquadronMethod() + Future jsonDecode(String source) async => json.decode(source); +} diff --git a/example/lib/json_decode_service.vm.g.dart b/example/lib/json_decode_service.vm.g.dart new file mode 100644 index 00000000..7ad9a226 --- /dev/null +++ b/example/lib/json_decode_service.vm.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// SquadronWorkerGenerator +// ************************************************************************** + +import 'package:squadron/squadron_service.dart'; +import 'json_decode_service.dart'; + +// VM entry point +void _start(Map command) => run($JsonDecodeServiceInitializer, command); + +dynamic $getJsonDecodeServiceActivator() => _start; diff --git a/example/lib/json_decode_service.worker.g.dart b/example/lib/json_decode_service.worker.g.dart new file mode 100644 index 00000000..d50372ab --- /dev/null +++ b/example/lib/json_decode_service.worker.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'json_decode_service.dart'; + +// ************************************************************************** +// SquadronWorkerGenerator +// ************************************************************************** + +// Operations map for JsonDecodeService +mixin $JsonDecodeServiceOperations on WorkerService { + @override + late final Map operations = + _getOperations(this as JsonDecodeService); + + static const int _$jsonDecodeId = 1; + + static Map _getOperations(JsonDecodeService svc) => { + _$jsonDecodeId: (r) => svc.jsonDecode(r.args[0]), + }; +} + +// Service initializer +JsonDecodeService $JsonDecodeServiceInitializer(WorkerRequest startRequest) => + JsonDecodeService(); + +// Worker for JsonDecodeService +class JsonDecodeServiceWorker extends Worker + with $JsonDecodeServiceOperations + implements JsonDecodeService { + JsonDecodeServiceWorker() : super($JsonDecodeServiceActivator); + + @override + Future jsonDecode(String source) => send( + $JsonDecodeServiceOperations._$jsonDecodeId, + args: [source], + token: null, + inspectRequest: false, + inspectResponse: false, + ); + + @override + Map get operations => WorkerService.noOperations; +} + +// Worker pool for JsonDecodeService +class JsonDecodeServiceWorkerPool extends WorkerPool + with $JsonDecodeServiceOperations + implements JsonDecodeService { + JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) + : super(() => JsonDecodeServiceWorker(), + concurrencySettings: concurrencySettings); + + @override + Future jsonDecode(String source) => + execute((w) => w.jsonDecode(source)); + + @override + Map get operations => WorkerService.noOperations; +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9975770b..3bce130d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: analyzer: http: built_collection: + squadron: ^4.3.0 dev_dependencies: build_runner: @@ -22,6 +23,7 @@ dev_dependencies: built_value_generator: dart_code_metrics: ^4.8.1 lints: ^2.0.0 + squadron_builder: ^0.9.0 dependency_overrides: chopper: diff --git a/faq.md b/faq.md index 40658686..fca603c6 100644 --- a/faq.md +++ b/faq.md @@ -169,3 +169,197 @@ interceptors: [ ``` The actual implementation of the algorithm above may vary based on how the backend API - more precisely the login and session handling - of your app looks like. + +## Decoding JSON using Isolates + +Sometimes you want to decode JSON outside the main thread in order to reduce janking. In this example we're going to go +even further and implement a Worker Pool using [Squadron](https://pub.dev/packages/squadron/install) which can +dynamically spawn a maximum number of Workers as they become needed. + +#### Install the dependencies + +- [squadron](https://pub.dev/packages/squadron) +- [squadron_builder](https://pub.dev/packages/squadron_builder) +- [json_annotation](https://pub.dev/packages/json_annotation) +- [json_serializable](https://pub.dev/packages/json_serializable) + +#### Write a JSON decode service + +We'll leverage [squadron_builder](https://pub.dev/packages/squadron_builder) and the power of code generation. + +```dart +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:squadron/squadron.dart'; +import 'package:squadron/squadron_annotations.dart'; + +import 'json_decode_service.activator.g.dart'; + +part 'json_decode_service.worker.g.dart'; + +@SquadronService() +class JsonDecodeService extends WorkerService with $JsonDecodeServiceOperations { + @SquadronMethod() + Future jsonDecode(String source) async => json.decode(source); +} +``` + +Extracted from the [full example here](example/lib/json_decode_service.dart). + +#### Write a custom JsonConverter + +Using [json_serializable](https://pub.dev/packages/json_serializable) we'll create a [JsonConverter](https://github.com/lejard-h/chopper/blob/master/chopper/lib/src/interceptor.dart#L228) +which works with or without a [WorkerPool](https://github.com/d-markey/squadron#features). + +```dart +import 'dart:async' show FutureOr; +import 'dart:convert' show jsonDecode; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/json_decode_service.dart'; +import 'package:chopper_example/json_serializable.dart'; + +typedef JsonFactory = T Function(Map json); + +class JsonSerializableWorkerPoolConverter extends JsonConverter { + const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]); + + final Map factories; + + /// Make the WorkerPool optional so that the JsonConverter still works without it + final JsonDecodeServiceWorkerPool? workerPool; + + /// By overriding tryDecodeJson we give our JsonConverter + /// the ability to decode JSON in an Isolate. + @override + FutureOr tryDecodeJson(String data) async { + try { + return workerPool != null + ? await workerPool!.jsonDecode(data) + : jsonDecode(data); + } catch (error) { + print(error); + + chopperLogger.warning(error); + + return data; + } + } + + T? _decodeMap(Map values) { + final jsonFactory = factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + return null; + } + + return jsonFactory(values); + } + + List _decodeList(Iterable values) => + values.where((v) => v != null).map((v) => _decode(v)).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); + + if (entity is Map) return _decodeMap(entity as Map); + + return entity; + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + final jsonRes = await super.convertResponse(response); + + return jsonRes.copyWith(body: _decode(jsonRes.body)); + } + + @override + FutureOr convertError(Response response) async { + final jsonRes = await super.convertError(response); + + return jsonRes.copyWith( + body: ResourceError.fromJsonFactory(jsonRes.body), + ); + } +} +``` + +Extracted from the [full example here](example/bin/main_json_serializable_squadron_worker_pool.dart). + +#### Code generation + +It goes without saying that running the code generation is a pre-requisite at this stage + +```bash +flutter pub run build_runner build +``` + +#### Configure a WorkerPool and run the example + +```dart +/// inspired by https://github.com/d-markey/squadron_sample/blob/main/lib/main.dart +void initSquadron(String id) { + Squadron.setId(id); + Squadron.setLogger(ConsoleSquadronLogger()); + Squadron.logLevel = SquadronLogLevel.all; + Squadron.debugMode = true; +} + +Future main() async { + /// initialize Squadron before using it + initSquadron('worker_pool_example'); + + final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool( + // Set whatever you want here + concurrencySettings: ConcurrencySettings.oneCpuThread, + ); + + /// start the Worker Pool + await jsonDecodeServiceWorkerPool.start(); + + /// Instantiate the JsonConverter from above + final converter = JsonSerializableWorkerPoolConverter( + { + Resource: Resource.fromJsonFactory, + }, + /// make sure to provide the WorkerPool to the JsonConverter + jsonDecodeServiceWorkerPool, + ); + + /// Instantiate a ChopperClient + final chopper = ChopperClient( + client: client, + baseUrl: 'http://localhost:8000', + // bind your object factories here + converter: converter, + errorConverter: converter, + services: [ + // the generated service + MyService.create(), + ], + /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ + interceptors: [authHeader], + ); + + /// Do stuff with myService + final myService = chopper.getService(); + + /// ...stuff... + + /// stop the Worker Pool once done + jsonDecodeServiceWorkerPool.stop(); +} +``` + +[The full example can be found here](example/bin/main_json_serializable_squadron_worker_pool.dart). + +#### Further reading + +This barely scratches the surface. If you want to know more about [squadron](https://github.com/d-markey/squadron) and +[squadron_builder](https://github.com/d-markey/squadron_builder) make sure to head over to their respective repositories. + +[David Markey](https://github.com/d-markey]), the author of squadron, was kind enough as to provide us with an [excellent Flutter example](https://github.com/d-markey/squadron_builder) using +both packages. \ No newline at end of file