diff --git a/analysis_options.yaml b/analysis_options.yaml index dfd85fe..ded25a4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,7 +6,10 @@ analyzer: - "build/**" # Codegen - "lib/**.g.dart" - - "lib/src/protobuf/*" + - "lib/src/protobuf/client.pb.dart" + - "lib/src/protobuf/client.pbenum.dart" + - "lib/src/protobuf/client.pbjson.dart" + - "lib/src/protobuf/client.pbserver.dart" - "lib/src/model/pubspec.yaml.g.dart" - "lib/src.old/transport/protobuf/*" - "lib/src.old/transport/pubspec.yaml.g.dart" diff --git a/lib/spinify.dart b/lib/spinify.dart index b87b3b8..67f1dd9 100644 --- a/lib/spinify.dart +++ b/lib/spinify.dart @@ -1,4 +1,5 @@ library spinify; +export 'src/model/config.dart'; export 'src/model/state.dart'; export 'src/spinify_impl.dart' show Spinify; diff --git a/lib/src/model/command.dart b/lib/src/model/command.dart index 67a8b5e..5bf8953 100644 --- a/lib/src/model/command.dart +++ b/lib/src/model/command.dart @@ -292,7 +292,8 @@ final class SpinifyRefreshRequest extends SpinifyCommand { String get type => 'RefreshRequest'; /// Token to refresh. - final String? token; + /// Token should not be null or empty string. + final String token; } /// {@macro command} diff --git a/lib/src/model/config.dart b/lib/src/model/config.dart index 2182932..b971e9b 100644 --- a/lib/src/model/config.dart +++ b/lib/src/model/config.dart @@ -54,6 +54,7 @@ final class SpinifyConfig { this.timeout = const Duration(seconds: 15), this.serverPingDelay = const Duration(seconds: 8), Map? headers, + this.logger, }) : headers = Map.unmodifiable( headers ?? const {}), client = client ?? @@ -112,6 +113,27 @@ final class SpinifyConfig { /// If not specified, the timeout will be 15 seconds. final Duration timeout; + /// 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 + final void Function( + int level, + String event, + String message, + Map context, + )? logger; + @override String toString() => 'SpinifyConfig{}'; } diff --git a/lib/src/model/transport_interface.dart b/lib/src/model/transport_interface.dart index cd6af24..ab16972 100644 --- a/lib/src/model/transport_interface.dart +++ b/lib/src/model/transport_interface.dart @@ -1,4 +1,5 @@ import 'command.dart'; +import 'config.dart'; import 'reply.dart'; /// Create a Spinify transport @@ -7,8 +8,8 @@ typedef CreateSpinifyTransport = Future Function( /// URL for the connection String url, - /// Additional headers for the connection (optional) - Map headers, + /// Spinify client configuration + SpinifyConfig config, ); /// Spinify transport interface. diff --git a/lib/src/protobuf/protobuf_codec.dart b/lib/src/protobuf/protobuf_codec.dart index bc4a00d..1e1b16a 100644 --- a/lib/src/protobuf/protobuf_codec.dart +++ b/lib/src/protobuf/protobuf_codec.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:meta/meta.dart'; -import 'package:protobuf/protobuf.dart' as pb; import '../model/channel_push.dart'; import '../model/client_info.dart'; @@ -11,14 +10,35 @@ import '../model/reply.dart'; import '../model/stream_position.dart'; import 'client.pb.dart' as pb; -/// SpinifyCommand --> List encoder. +/// SpinifyCommand --> Protobuf Command encoder. final class ProtobufCommandEncoder - extends Converter> { + extends Converter { /// SpinifyCommand --> List encoder. - const ProtobufCommandEncoder(); + const ProtobufCommandEncoder([this.logger]); + + /// 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 + final void Function( + int level, + String event, + String message, + Map context, + )? logger; @override - List convert(SpinifyCommand input) { + pb.Command convert(SpinifyCommand input) { final cmd = pb.Command(id: input.id); switch (input) { case SpinifySendRequest send: @@ -109,36 +129,64 @@ final class ProtobufCommandEncoder token: subRefresh.token, ); } - assert(() { + /* assert(() { print('Command > ${cmd.toProto3Json()}'); return true; - }()); + }()); */ + /* final buffer = pb.CodedBufferWriter(); pb.writeToCodedBufferWriter(buffer); return buffer.toBuffer(); */ - final commandData = cmd.writeToBuffer(); + + /* final commandData = cmd.writeToBuffer(); final length = commandData.lengthInBytes; final writer = pb.CodedBufferWriter() ..writeInt32NoTag(length); //..writeRawBytes(commandData); - return writer.toBuffer() + commandData; + return writer.toBuffer() + commandData; */ + + return cmd; } } -/// List --> SpinifyReply decoder. -final class ProtobufReplyDecoder extends Converter, SpinifyReply> { +/// Protobuf Reply --> SpinifyReply decoder. +final class ProtobufReplyDecoder extends Converter { /// List --> SpinifyCommand decoder. - const ProtobufReplyDecoder(); + const ProtobufReplyDecoder([this.logger]); + + /// 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 + final void Function( + int level, + String event, + String message, + Map context, + )? logger; @override - SpinifyReply convert(List input) { - final reader = pb.CodedBufferReader(input); + SpinifyReply convert(pb.Reply input) { + //final reader = pb.CodedBufferReader(input); //while (!reader.isAtEnd()) { - final reply = pb.Reply(); - reader.readMessage(reply, pb.ExtensionRegistry.EMPTY); - assert(() { + //final reply = pb.Reply(); + //reader.readMessage(reply, pb.ExtensionRegistry.EMPTY); + final reply = input; + + /* assert(() { print('Reply < ${reply.toProto3Json()}'); return true; - }()); + }()); */ + if (reply.hasPush()) { return _decodePush(reply.push); } else if (reply.hasId() && reply.id > 0) { @@ -158,7 +206,7 @@ final class ProtobufReplyDecoder extends Converter, SpinifyReply> { ); } //} - assert(reader.isAtEnd(), 'Data is not fully consumed'); + //assert(reader.isAtEnd(), 'Data is not fully consumed'); } /* diff --git a/lib/src/spinify_impl.dart b/lib/src/spinify_impl.dart index 067b3a2..b406cf6 100644 --- a/lib/src/spinify_impl.dart +++ b/lib/src/spinify_impl.dart @@ -49,21 +49,56 @@ abstract base class SpinifyBase implements ISpinify { @mustCallSuper void _init() { _createTransport = $create$WS$PB$Transport; + config.logger?.call( + 3, + 'init', + 'Spinify client initialized', + { + 'config': config, + }, + ); } /// On connect to the server. @mustCallSuper - Future _onConnect(String url) async {} + Future _onConnected() async {} @mustCallSuper - Future _onReply(SpinifyReply reply) async {} + Future _onReply(SpinifyReply reply) async { + config.logger?.call( + 0, + 'reply', + 'Reply ${reply.type}{id: ${reply.id}} received', + { + 'reply': reply, + }, + ); + } /// On disconnect from the server. @mustCallSuper - Future _onDisconnect() async {} + Future _onDisconnected() async { + config.logger?.call( + 2, + 'disconnected', + 'Disconnected', + { + 'state': state, + }, + ); + } @override - Future close() async {} + Future close() async { + config.logger?.call( + 3, + 'closed', + 'Closed', + { + 'state': state, + }, + ); + } } /// Base mixin for Spinify client state management. @@ -81,17 +116,28 @@ base mixin SpinifyStateMixin on SpinifyBase { StreamController.broadcast(); @nonVirtual - void _setState(SpinifyState state) => _statesController.add(_state = state); + void _setState(SpinifyState state) { + final previous = _state; + _statesController.add(_state = state); + config.logger?.call( + 2, + 'state_changed', + 'State changed from $previous to $state', + { + 'previous': previous, + 'state': state, + }, + ); + } @override - Future _onConnect(String url) async { - _setState(SpinifyState$Connecting(url: url)); - await super._onConnect(url); + Future _onConnected() async { + await super._onConnected(); } @override - Future _onDisconnect() async { - await super._onDisconnect(); + Future _onDisconnected() async { + await super._onDisconnected(); if (!state.isDisconnected) _setState(SpinifyState$Disconnected()); } @@ -105,8 +151,9 @@ base mixin SpinifyStateMixin on SpinifyBase { /// Base mixin for Spinify command sending. base mixin SpinifyCommandMixin on SpinifyBase { - final Map> _replies = - >{}; + final Map completer})> + _replies = + completer})>{}; @override Future send(List data) => _sendCommandAsync(SpinifySendRequest( @@ -116,46 +163,134 @@ base mixin SpinifyCommandMixin on SpinifyBase { )); Future _sendCommand(SpinifyCommand command) async { + config.logger?.call( + 0, + 'send_command_begin', + 'Command ${command.type}{id: ${command.id}} sent begin', + { + 'command': command, + }, + ); try { assert(command.id > -1, 'Command ID should be greater or equal to 0'); assert(_replies[command.id] == null, 'Command ID should be unique'); assert(_transport != null, 'Transport is not connected'); assert(!state.isClosed, 'State is closed'); - final completer = _replies[command.id] = Completer(); - await _sendCommandAsync(command); - return await completer.future.timeout(config.timeout); + final completer = Completer(); + _replies[command.id] = (command: command, completer: completer); + await _transport?.send(command); // await _sendCommandAsync(command); + final result = await completer.future.timeout(config.timeout); + config.logger?.call( + 2, + 'send_command_success', + 'Command ${command.type}{id: ${command.id}} sent successfully', + { + 'command': command, + 'result': result, + }, + ); + return result; } on Object catch (error, stackTrace) { - final completer = _replies.remove(command.id); - if (completer != null && !completer.isCompleted) - completer.completeError(error, stackTrace); + final tuple = _replies.remove(command.id); + if (tuple != null && !tuple.completer.isCompleted) { + tuple.completer.completeError(error, stackTrace); + config.logger?.call( + 4, + 'send_command_error', + 'Error sending command ${command.type}{id: ${command.id}}', + { + 'command': command, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + } rethrow; } } Future _sendCommandAsync(SpinifyCommand command) async { - assert(command.id > -1, 'Command ID should be greater or equal to 0'); - assert(_transport != null, 'Transport is not connected'); - assert(!state.isClosed, 'State is closed'); - await _transport?.send(command); + config.logger?.call( + 0, + 'send_command_async_begin', + 'Comand ${command.type}{id: ${command.id}} sent async begin', + { + 'command': command, + }, + ); + try { + assert(command.id > -1, 'Command ID should be greater or equal to 0'); + assert(_transport != null, 'Transport is not connected'); + assert(!state.isClosed, 'State is closed'); + await _transport?.send(command); + config.logger?.call( + 2, + 'send_command_async_success', + 'Command sent ${command.type}{id: ${command.id}} async successfully', + { + 'command': command, + }, + ); + } on Object catch (error, stackTrace) { + config.logger?.call( + 4, + 'send_command_async_error', + 'Error sending command ${command.type}{id: ${command.id}} async', + { + 'command': command, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + rethrow; + } } @override Future _onReply(SpinifyReply reply) async { assert(reply.id > -1, 'Reply ID should be greater or equal to 0'); if (reply.id case int id when id > 0) { - final completer = _replies.remove(id); - assert(completer != null, 'Reply completer not found'); + final completer = _replies.remove(id)?.completer; + assert( + completer != null, + 'Reply completer not found', + ); + assert( + completer?.isCompleted == false, + 'Reply completer already completed', + ); completer?.complete(reply); } await super._onReply(reply); } @override - Future _onDisconnect() async { + Future _onDisconnected() async { + config.logger?.call( + 2, + 'disconnected', + 'Disconnected from server', + {}, + ); late final error = StateError('Client is disconnected'); - for (final completer in _replies.values) completer.completeError(error); + late final stackTrace = StackTrace.current; + for (final tuple in _replies.values) { + if (tuple.completer.isCompleted) continue; + tuple.completer.completeError(error); + config.logger?.call( + 4, + 'disconnected_reply_error', + 'Reply for command ${tuple.command.type}{id: ${tuple.command.id}} ' + 'error on disconnect', + { + 'command': tuple.command, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + } _replies.clear(); - await super._onDisconnect(); + await super._onDisconnected(); } } @@ -182,9 +317,9 @@ base mixin SpinifyConnectionMixin _reconnectUrl = url; // Create new transport. - _transport = await _createTransport(url, config.headers) + _transport = await _createTransport(url, config) ..onReply = _onReply - ..onDisconnect = () => _onDisconnect().ignore(); + ..onDisconnect = () => _onDisconnected().ignore(); // Prepare connect request. final SpinifyConnectRequest request; @@ -223,9 +358,32 @@ base mixin SpinifyConnectionMixin // Notify ready. if (!completer.isCompleted) completer.complete(); _readyCompleter = null; + + await _onConnected(); + + config.logger?.call( + 2, + 'connected', + 'Connected to server with $url successfully', + { + 'url': url, + 'request': request, + 'result': reply, + }, + ); } on Object catch (error, stackTrace) { if (!completer.isCompleted) completer.completeError(error, stackTrace); _readyCompleter = null; + config.logger?.call( + 5, + 'connect_error', + 'Error connecting to server $url', + { + 'url': url, + 'error': error, + 'stackTrace': stackTrace, + }, + ); rethrow; } } @@ -245,26 +403,63 @@ base mixin SpinifyConnectionMixin ) when expires && ttl != null) { final duration = ttl.difference(DateTime.now()) - config.timeout; if (duration < Duration.zero) { + config.logger?.call( + 4, + 'refresh_connection_cancelled', + 'Spinify token TTL is too short for refresh connection', + { + 'url': url, + 'duration': duration, + 'ttl': ttl, + }, + ); assert(false, 'Token TTL is too short'); return; } _refreshTimer = Timer(duration, () async { if (!state.isConnected) return; final token = await config.getToken?.call(); - assert(token == null || token.length > 5, 'Spinify JWT is too short'); - if (token == null) return; + if (token == null || token.isEmpty) { + assert(token == null || token.length > 5, 'Spinify JWT is too short'); + config.logger?.call( + 4, + 'refresh_connection_cancelled', + 'Spinify JWT is empty or too short for refresh connection', + { + 'url': url, + 'token': token, + }, + ); + return; + } final request = SpinifyRefreshRequest( id: _getNextCommandId(), timestamp: DateTime.now(), token: token, ); - final reply = await _sendCommand(request); + final SpinifyRefreshResult result; + try { + result = await _sendCommand(request); + } on Object catch (error, stackTrace) { + config.logger?.call( + 5, + 'refresh_connection_error', + 'Error refreshing connection', + { + 'url': url, + 'command': request, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + return; + } _setState(SpinifyState$Connected( url: url, - client: reply.client, - version: reply.version, - expires: reply.expires, - ttl: reply.ttl, + client: result.client, + version: result.version, + expires: result.expires, + ttl: result.ttl, node: node, pingInterval: pingInterval, sendPong: sendPong, @@ -272,6 +467,15 @@ base mixin SpinifyConnectionMixin data: data, )); _setUpRefreshConnection(); + config.logger?.call( + 2, + 'refresh_connection_success', + 'Successfully refreshed connection to $url', + { + 'request': request, + 'result': result, + }, + ); }); } } @@ -287,14 +491,14 @@ base mixin SpinifyConnectionMixin _reconnectUrl = null; if (state.isDisconnected) return Future.value(); await _transport?.disconnect(1000, 'Client disconnecting'); - await _onDisconnect(); + await _onDisconnected(); } @override - Future _onDisconnect() async { + Future _onDisconnected() async { _refreshTimer?.cancel(); _transport = null; - await super._onDisconnect(); + await super._onDisconnected(); } @override @@ -339,7 +543,19 @@ base mixin SpinifyPingPongMixin pingInterval + config.serverPingDelay, () { // Reconnect if no pong received. - if (state case SpinifyState$Connected(:String url)) connect(url); + if (state case SpinifyState$Connected(:String url)) { + config.logger?.call( + 4, + 'no_pong_reconnect', + 'No pong from server - reconnecting', + { + 'url': url, + 'pingInterval': pingInterval, + 'serverPingDelay': config.serverPingDelay, + }, + ); + connect(url); + } /* disconnect( SpinifyConnectingCode.noPing, 'No ping from server', @@ -350,25 +566,35 @@ base mixin SpinifyPingPongMixin } @override - Future _onConnect(String url) async { + Future _onConnected() async { _tearDownPingTimer(); - await super._onConnect(url); + await super._onConnected(); _restartPingTimer(); } @override Future _onReply(SpinifyReply reply) async { if (reply is SpinifyServerPing) { - await _sendCommandAsync(SpinifyPingRequest(timestamp: DateTime.now())); + final command = SpinifyPingRequest(timestamp: DateTime.now()); + await _sendCommandAsync(command); + config.logger?.call( + 0, + 'server_ping_received', + 'Ping from server received, pong sent', + { + 'ping': reply, + 'pong': command, + }, + ); _restartPingTimer(); } await super._onReply(reply); } @override - Future _onDisconnect() async { + Future _onDisconnected() async { _tearDownPingTimer(); - await super._onDisconnect(); + await super._onDisconnected(); } @override diff --git a/lib/src/transport_ws_pb_js.dart b/lib/src/transport_ws_pb_js.dart index 1268f77..3456831 100644 --- a/lib/src/transport_ws_pb_js.dart +++ b/lib/src/transport_ws_pb_js.dart @@ -1,11 +1,12 @@ import 'package:meta/meta.dart'; +import 'model/config.dart'; import 'model/transport_interface.dart'; /// Create a WebSocket Protocol Buffers transport. @internal Future $create$WS$PB$Transport( String url, - Map headers, + SpinifyConfig config, ) => throw UnimplementedError(); diff --git a/lib/src/transport_ws_pb_stub.dart b/lib/src/transport_ws_pb_stub.dart index 1268f77..3456831 100644 --- a/lib/src/transport_ws_pb_stub.dart +++ b/lib/src/transport_ws_pb_stub.dart @@ -1,11 +1,12 @@ import 'package:meta/meta.dart'; +import 'model/config.dart'; import 'model/transport_interface.dart'; /// Create a WebSocket Protocol Buffers transport. @internal Future $create$WS$PB$Transport( String url, - Map headers, + SpinifyConfig config, ) => throw UnimplementedError(); diff --git a/lib/src/transport_ws_pb_vm.dart b/lib/src/transport_ws_pb_vm.dart index 6818803..d10cfea 100644 --- a/lib/src/transport_ws_pb_vm.dart +++ b/lib/src/transport_ws_pb_vm.dart @@ -1,26 +1,30 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io' as io; import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart' as pb; import 'model/command.dart'; +import 'model/config.dart'; import 'model/reply.dart'; import 'model/transport_interface.dart'; +import 'protobuf/client.pb.dart' as pb; import 'protobuf/protobuf_codec.dart'; /// Create a WebSocket Protocol Buffers transport. @internal Future $create$WS$PB$Transport( String url, - Map headers, + SpinifyConfig config, ) async { // ignore: close_sinks final socket = await io.WebSocket.connect( url, - headers: headers, + headers: config.headers, protocols: {'centrifuge-protobuf'}, ); - final transport = SpinifyTransport$WS$PB$VM(socket); + final transport = SpinifyTransport$WS$PB$VM(socket, config); // 0 CONNECTING Socket has been created. The connection is not yet open. // 1 OPEN The connection is open and ready to communicate. // 2 CLOSING The connection is in the process of closing. @@ -32,7 +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) { + SpinifyTransport$WS$PB$VM(this._socket, this._config) + : _encoder = switch (_config.logger) { + null => const ProtobufCommandEncoder(), + _ => ProtobufCommandEncoder(_config.logger), + }, + _decoder = switch (_config.logger) { + null => const ProtobufReplyDecoder(), + _ => ProtobufReplyDecoder(_config.logger), + } { _subscription = _socket.listen( _onData, cancelOnError: false, @@ -44,6 +56,9 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { } final io.WebSocket _socket; + final SpinifyConfig _config; + final Converter _encoder; + final Converter _decoder; late final StreamSubscription _subscription; void Function(SpinifyReply reply)? _onReply; @@ -58,21 +73,88 @@ final class SpinifyTransport$WS$PB$VM implements ISpinifyTransport { void Function()? _onDisconnect; void _onData(Object? bytes) { - const decoder = ProtobufReplyDecoder(); if (bytes is! List || bytes.isEmpty) { assert(false, 'Data is not byte array'); return; } - final reply = decoder.convert(bytes); assert(_onReply != null, 'Reply handler is not set'); - _onReply?.call(reply); + final reader = pb.CodedBufferReader(bytes); + while (!reader.isAtEnd()) { + try { + final message = pb.Reply(); + reader.readMessage(message, pb.ExtensionRegistry.EMPTY); + final reply = _decoder.convert(message); + _onReply?.call(reply); + _config.logger?.call( + 1, + 'receive_reply', + 'Reply ${reply.type}{id: ${reply.id}} received', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'bytes': bytes, + 'length': bytes.length, + 'reply': reply, + 'protobuf': message, + }, + ); + } on Object catch (error, stackTrace) { + _config.logger?.call( + 5, + 'receive_reply_error', + 'Error reading reply message', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'bytes': bytes, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + assert(false, 'Error reading message: $error'); + continue; + } + } } @override Future send(SpinifyCommand command) async { - const encoder = ProtobufCommandEncoder(); - final bytes = encoder.convert(command); - _socket.add(bytes); + try { + final message = _encoder.convert(command); + final commandData = message.writeToBuffer(); + final length = commandData.lengthInBytes; + final writer = pb.CodedBufferWriter() + ..writeInt32NoTag(length); //..writeRawBytes(commandData); + final bytes = writer.toBuffer() + commandData; + _socket.add(bytes); + _config.logger?.call( + 1, + 'send_command', + 'Command ${command.type}{id: ${command.id}} sent', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'command': command, + 'protobuf': message, + 'length': bytes.length, + 'bytes': bytes, + }, + ); + } on Object catch (error, stackTrace) { + _config.logger?.call( + 5, + 'send_command_error', + 'Error sending command ${command.type}{id: ${command.id}}', + { + 'protocol': 'protobuf', + 'transport': 'websocket', + 'command': command, + 'error': error, + 'stackTrace': stackTrace, + }, + ); + rethrow; + } } @override diff --git a/test/smoke/smoke_test.dart b/test/smoke/smoke_test.dart index afcdd18..a17cd43 100644 --- a/test/smoke/smoke_test.dart +++ b/test/smoke/smoke_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'dart:convert'; import 'package:spinify/spinify.dart'; @@ -20,11 +22,16 @@ void main() { }); test('Connect_and_refresh', () async { - final client = Spinify(); + final client = Spinify( + config: SpinifyConfig( + logger: (level, event, message, context) => + print('[$event] $message'), + ), + ); await client.connect(url); expect(client.state, isA()); //await client.ping(); - await Future.delayed(const Duration(seconds: 360)); + await Future.delayed(const Duration(seconds: 60)); await client.disconnect(); expect(client.state, isA()); await client.close();