From 2de2f82caae996fe700e2e2f3fe5027521344c1b Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Mon, 13 May 2024 14:01:20 +0400 Subject: [PATCH] Refactor SpinifyCommandMixin error handling and connection logic --- lib/src/model/config.dart | 122 +++++++++++++++++++++++-- lib/src/model/transport_interface.dart | 2 +- lib/src/protobuf/protobuf_codec.dart | 15 +-- lib/src/spinify_impl.dart | 44 ++++----- lib/src/transport_ws_pb_vm.dart | 29 +++--- 5 files changed, 157 insertions(+), 55 deletions(-) diff --git a/lib/src/model/config.dart b/lib/src/model/config.dart index b971e9b..dfe1c55 100644 --- a/lib/src/model/config.dart +++ b/lib/src/model/config.dart @@ -28,6 +28,121 @@ typedef SpinifyTokenCallback = FutureOr Function(); /// {@category Entity} typedef SpinifyConnectionPayloadCallback = FutureOr?> Function(); +/// Log level for logger +extension type const SpinifyLogLevel._(int level) { + /// Log level: debug + @literal + const SpinifyLogLevel.debug() : level = 0; + + /// Log level: transport + @literal + const SpinifyLogLevel.transport() : level = 1; + + /// Log level: config + @literal + const SpinifyLogLevel.config() : level = 2; + + /// Log level: info + @literal + const SpinifyLogLevel.info() : level = 3; + + /// Log level: warning + @literal + const SpinifyLogLevel.warning() : level = 4; + + /// Log level: error + @literal + const SpinifyLogLevel.error() : level = 5; + + /// Log level: critical + @literal + const SpinifyLogLevel.critical() : level = 6; + + /// Pattern matching on log level + T map({ + required T Function() debug, + required T Function() transport, + required T Function() config, + required T Function() info, + required T Function() warning, + required T Function() error, + required T Function() critical, + }) => + switch (level) { + 0 => debug(), + 1 => transport(), + 2 => config(), + 3 => info(), + 4 => warning(), + 5 => error(), + 6 => critical(), + _ => throw AssertionError('Unknown log level: $level'), + }; + + /// Pattern matching on log level + T maybeMap({ + required T Function() orElse, + T Function()? debug, + T Function()? transport, + T Function()? config, + T Function()? info, + T Function()? warning, + T Function()? error, + T Function()? critical, + }) => + map( + debug: debug ?? orElse, + transport: transport ?? orElse, + config: config ?? orElse, + info: info ?? orElse, + warning: warning ?? orElse, + error: error ?? orElse, + critical: critical ?? orElse, + ); + + /// Pattern matching on log level + T? mapOrNull({ + T Function()? debug, + T Function()? transport, + T Function()? config, + T Function()? info, + T Function()? warning, + T Function()? error, + T Function()? critical, + }) => + maybeMap( + orElse: () => null, + debug: debug, + transport: transport, + config: config, + info: info, + warning: warning, + error: error, + critical: critical, + ); +} + +/// Logger function to use for logging. +/// If not specified, the logger will be disabled. +/// The logger function is called with the following arguments: +/// - [level] - the log verbose level 0..6 +/// * 0 - debug +/// * 1 - transport +/// * 2 - config +/// * 3 - info +/// * 4 - warning +/// * 5 - error +/// * 6 - critical +/// - [event] - the log event, unique type of log event +/// - [message] - the log message +/// - [context] - the log context data +typedef SpinifyLogger = void Function( + SpinifyLogLevel level, + String event, + String message, + Map context, +); + /// {@template spinify_config} /// Spinify client common options. /// @@ -127,12 +242,7 @@ final class SpinifyConfig { /// - [event] - the log event, unique type of log event /// - [message] - the log message /// - [context] - the log context data - final void Function( - int level, - String event, - String message, - Map context, - )? logger; + final SpinifyLogger? logger; @override String toString() => 'SpinifyConfig{}'; diff --git a/lib/src/model/transport_interface.dart b/lib/src/model/transport_interface.dart index ab16972..890e98b 100644 --- a/lib/src/model/transport_interface.dart +++ b/lib/src/model/transport_interface.dart @@ -4,7 +4,7 @@ import 'reply.dart'; /// Create a Spinify transport /// (e.g. WebSocket or gRPC with JSON or Protocol Buffers). -typedef CreateSpinifyTransport = Future Function( +typedef SpinifyTransportBuilder = Future Function( /// URL for the connection String url, diff --git a/lib/src/protobuf/protobuf_codec.dart b/lib/src/protobuf/protobuf_codec.dart index 8438d7a..c3651cc 100644 --- a/lib/src/protobuf/protobuf_codec.dart +++ b/lib/src/protobuf/protobuf_codec.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../model/channel_push.dart'; import '../model/client_info.dart'; import '../model/command.dart'; +import '../model/config.dart'; import '../model/reply.dart'; import '../model/stream_position.dart'; import 'client.pb.dart' as pb; @@ -30,12 +31,7 @@ final class ProtobufCommandEncoder /// - [event] - the log event, unique type of log event /// - [message] - the log message /// - [context] - the log context data - final void Function( - int level, - String event, - String message, - Map context, - )? logger; + final SpinifyLogger? logger; @override pb.Command convert(SpinifyCommand input) { @@ -167,12 +163,7 @@ final class ProtobufReplyDecoder extends Converter { /// - [event] - the log event, unique type of log event /// - [message] - the log message /// - [context] - the log context data - final void Function( - int level, - String event, - String message, - Map context, - )? logger; + final SpinifyLogger? logger; @override SpinifyReply convert(pb.Reply input) { diff --git a/lib/src/spinify_impl.dart b/lib/src/spinify_impl.dart index b059074..c69eafe 100644 --- a/lib/src/spinify_impl.dart +++ b/lib/src/spinify_impl.dart @@ -42,7 +42,7 @@ abstract base class SpinifyBase implements ISpinify { @nonVirtual final SpinifyConfig config; - late final CreateSpinifyTransport _createTransport; + late final SpinifyTransportBuilder _createTransport; ISpinifyTransport? _transport; /// Client initialization (from constructor). @@ -50,7 +50,7 @@ abstract base class SpinifyBase implements ISpinify { void _init() { _createTransport = $create$WS$PB$Transport; config.logger?.call( - 3, + const SpinifyLogLevel.info(), 'init', 'Spinify client initialized', { @@ -66,7 +66,7 @@ abstract base class SpinifyBase implements ISpinify { @mustCallSuper Future _onReply(SpinifyReply reply) async { config.logger?.call( - 0, + const SpinifyLogLevel.debug(), 'reply', 'Reply ${reply.type}{id: ${reply.id}} received', { @@ -79,7 +79,7 @@ abstract base class SpinifyBase implements ISpinify { @mustCallSuper Future _onDisconnected() async { config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'disconnected', 'Disconnected', { @@ -91,7 +91,7 @@ abstract base class SpinifyBase implements ISpinify { @override Future close() async { config.logger?.call( - 3, + const SpinifyLogLevel.info(), 'closed', 'Closed', { @@ -120,7 +120,7 @@ base mixin SpinifyStateMixin on SpinifyBase { final previous = _state; _statesController.add(_state = state); config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'state_changed', 'State changed from $previous to $state', { @@ -164,7 +164,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { Future _sendCommand(SpinifyCommand command) async { config.logger?.call( - 0, + const SpinifyLogLevel.debug(), 'send_command_begin', 'Command ${command.type}{id: ${command.id}} sent begin', { @@ -181,7 +181,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { await _transport?.send(command); // await _sendCommandAsync(command); final result = await completer.future.timeout(config.timeout); config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'send_command_success', 'Command ${command.type}{id: ${command.id}} sent successfully', { @@ -195,7 +195,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { if (tuple != null && !tuple.completer.isCompleted) { tuple.completer.completeError(error, stackTrace); config.logger?.call( - 4, + const SpinifyLogLevel.warning(), 'send_command_error', 'Error sending command ${command.type}{id: ${command.id}}', { @@ -211,7 +211,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { Future _sendCommandAsync(SpinifyCommand command) async { config.logger?.call( - 0, + const SpinifyLogLevel.debug(), 'send_command_async_begin', 'Comand ${command.type}{id: ${command.id}} sent async begin', { @@ -224,7 +224,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { assert(!state.isClosed, 'State is closed'); await _transport?.send(command); config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'send_command_async_success', 'Command sent ${command.type}{id: ${command.id}} async successfully', { @@ -233,7 +233,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { ); } on Object catch (error, stackTrace) { config.logger?.call( - 4, + const SpinifyLogLevel.warning(), 'send_command_async_error', 'Error sending command ${command.type}{id: ${command.id}} async', { @@ -267,7 +267,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { @override Future _onDisconnected() async { config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'disconnected', 'Disconnected from server', {}, @@ -278,7 +278,7 @@ base mixin SpinifyCommandMixin on SpinifyBase { if (tuple.completer.isCompleted) continue; tuple.completer.completeError(error); config.logger?.call( - 4, + const SpinifyLogLevel.warning(), 'disconnected_reply_error', 'Reply for command ${tuple.command.type}{id: ${tuple.command.id}} ' 'error on disconnect', @@ -362,7 +362,7 @@ base mixin SpinifyConnectionMixin await _onConnected(); config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'connected', 'Connected to server with $url successfully', { @@ -375,7 +375,7 @@ base mixin SpinifyConnectionMixin if (!completer.isCompleted) completer.completeError(error, stackTrace); _readyCompleter = null; config.logger?.call( - 5, + const SpinifyLogLevel.error(), 'connect_error', 'Error connecting to server $url', { @@ -404,7 +404,7 @@ base mixin SpinifyConnectionMixin final duration = ttl.difference(DateTime.now()) - config.timeout; if (duration < Duration.zero) { config.logger?.call( - 4, + const SpinifyLogLevel.warning(), 'refresh_connection_cancelled', 'Spinify token TTL is too short for refresh connection', { @@ -422,7 +422,7 @@ base mixin SpinifyConnectionMixin if (token == null || token.isEmpty) { assert(token == null || token.length > 5, 'Spinify JWT is too short'); config.logger?.call( - 4, + const SpinifyLogLevel.warning(), 'refresh_connection_cancelled', 'Spinify JWT is empty or too short for refresh connection', { @@ -442,7 +442,7 @@ base mixin SpinifyConnectionMixin result = await _sendCommand(request); } on Object catch (error, stackTrace) { config.logger?.call( - 5, + const SpinifyLogLevel.error(), 'refresh_connection_error', 'Error refreshing connection', { @@ -468,7 +468,7 @@ base mixin SpinifyConnectionMixin )); _setUpRefreshConnection(); config.logger?.call( - 2, + const SpinifyLogLevel.config(), 'refresh_connection_success', 'Successfully refreshed connection to $url', { @@ -545,7 +545,7 @@ base mixin SpinifyPingPongMixin // Reconnect if no pong received. if (state case SpinifyState$Connected(:String url)) { config.logger?.call( - 4, + const SpinifyLogLevel.warning(), 'no_pong_reconnect', 'No pong from server - reconnecting', { @@ -578,7 +578,7 @@ base mixin SpinifyPingPongMixin final command = SpinifyPingRequest(timestamp: DateTime.now()); await _sendCommandAsync(command); config.logger?.call( - 0, + const SpinifyLogLevel.debug(), 'server_ping_received', 'Ping from server received, pong sent', { diff --git a/lib/src/transport_ws_pb_vm.dart b/lib/src/transport_ws_pb_vm.dart index d577645..69e1a35 100644 --- a/lib/src/transport_ws_pb_vm.dart +++ b/lib/src/transport_ws_pb_vm.dart @@ -36,14 +36,15 @@ Future $create$WS$PB$Transport( /// Create a WebSocket Protocol Buffers transport. @internal final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { - SpinifyTransport$WS$PB$VM(this._socket, this._config) - : _encoder = switch (_config.logger) { + SpinifyTransport$WS$PB$VM(this._socket, SpinifyConfig config) + : _logger = config.logger, + _encoder = switch (config.logger) { null => const ProtobufCommandEncoder(), - _ => ProtobufCommandEncoder(_config.logger), + _ => ProtobufCommandEncoder(config.logger), }, - _decoder = switch (_config.logger) { + _decoder = switch (config.logger) { null => const ProtobufReplyDecoder(), - _ => ProtobufReplyDecoder(_config.logger), + _ => ProtobufReplyDecoder(config.logger), } { _subscription = _socket.listen( _onData, @@ -56,9 +57,9 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { } final io.WebSocket _socket; - final SpinifyConfig _config; final Converter _encoder; final Converter _decoder; + final SpinifyLogger? _logger; late final StreamSubscription _subscription; void Function(SpinifyReply reply)? _onReply; @@ -85,8 +86,8 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { reader.readMessage(message, pb.ExtensionRegistry.EMPTY); final reply = _decoder.convert(message); _onReply?.call(reply); - _config.logger?.call( - 1, + _logger?.call( + const SpinifyLogLevel.transport(), 'transport_on_reply', 'Reply ${reply.type}{id: ${reply.id}} received', { @@ -99,8 +100,8 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { }, ); } on Object catch (error, stackTrace) { - _config.logger?.call( - 5, + _logger?.call( + const SpinifyLogLevel.error(), 'transport_on_reply_error', 'Error reading reply message', { @@ -127,8 +128,8 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { ..writeInt32NoTag(length); //..writeRawBytes(commandData); final bytes = writer.toBuffer() + commandData; _socket.add(bytes); - _config.logger?.call( - 1, + _logger?.call( + const SpinifyLogLevel.transport(), 'transport_send', 'Command ${command.type}{id: ${command.id}} sent', { @@ -141,8 +142,8 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { }, ); } on Object catch (error, stackTrace) { - _config.logger?.call( - 5, + _logger?.call( + const SpinifyLogLevel.error(), 'transport_send_error', 'Error sending command ${command.type}{id: ${command.id}}', {